Skip to content

51 ‐ About continuous integration and delivery

Pierre-Yves Lapersonne edited this page Nov 26, 2024 · 1 revision

Today we host the project fully in this GitHub repository. However, because we do not want to leave outside certificates and provisioning profiles, we keep these sensitive files in our own GitLab CI runners and use a dedicated project to make the alpha, beta and production builds.

In the future, the demo app will be moved there so as to build it using both this public and open source repository (converted to a full Swift Package project) and internal private resources (like assets or brand elements).

About CI/CD with GitLabCI

You can setup in your side a GitLab CI runner which can trigger some Fastlane actions for example each night. However of course you will need distribution certificate (in .p12 format with private key) and the release provisioning profile in your runner keychain. Of course you will need also to fill secrets and environement variables. Do not expect use to share these secrets.

GitLab CI pipeline

You can copy/paste this file to define your YAML for GitLab CI.

# Software Name: OUDS iOS
# SPDX-FileCopyrightText: Copyright (c) Orange SA
# SPDX-License-Identifier: MIT

# Variables defined by user who wants to start the pipeline
variables:
  ALPHA_BRANCH_TO_BUILD:
    value: ""
    description: "Mandatory: The name of the branch to build as alpha version"
  ALPHA_ISSUES_NUMBERS:
    value: ""
    description: "Mandatory: The number(s) of the issue(s) in GitHub which will be implemented in ALPHA_BRANCH_TO_BUILD and built (e.g.: '42' or seperated with commas '42, 666, 1337'). Will be used also for GitHub notifications."
  GITHUB_REPOSITORY_NAME:
    value: "ouds-ios"
    description: "Mandatory: The name of the repository to use for builds (default: ouds-ios)"
  GITHUB_ORGANIZATION_NAME:
    value: "Orange-OpenSource"
    description: "Mandatory: The name of the GitHub organization containing the repository to use for builds (default: Orange-OpenSource)"
  BETA_BRANCH_TO_BUILD:
    value: "develop"
    description: "Mandatory: The name of the branch to build as beta version (default: develop)"
  PRODUCTION_BRANCH_TO_BUILD:
    value: "main"
    description: "Mandatory: The name of the branch to build as production version (default: main)"
  PATH_TO_IPA:
    value: "./tmp/ouds-ios/Showcase/build/Showcase.ipa"
    description: "Mandatory: The path to get the IPA for artifacts (default: ./tmp/ouds-ios/Showcase/build/Showcase.ipa)"
  PATH_TO_ZIP:
    value: "./tmp/ouds-ios/Showcase/build/oudsApp.zip"
    description: "Mandatory: The path to get the ZIP for artifacts (default: ./tmp/ouds-ios/Showcase/build/oudsApp.zip)"
  PATH_TO_APP_SOURCES:
    value: "./Showcase"
    description: "Mandatory: The path where the sources to build are (default: ./Showcase)"

# All stages for alpha, beta, production builds and releases
stages:
  - prepare-alpha
  - test-alpha
  - build-alpha
  - prepare-beta
  - test-beta
  - build-beta
  - prepare-production
  - build-production

# -------------------
# Alpha releases
# Dedicated branch to build on ask, triggered manualy, without Git tags

.common_alpha:
  tags:
    - xcode16 # Must match of course a registered and enabled runner with this label and Xcode 16 installed

.common_alpha_ios:
  extends: .common_alpha
  before_script:
    # Job fails with allowed error code if IOS_APP_COMMIT_SHA environment variable does not exist.
    # This IOS_APP_COMMIT_SHA variable is defined as environement variable in prepare-build-environment.sh
    - if [[ -z "$IOS_APP_COMMIT_SHA" ]]; then exit 81680085; fi
    - ./download_github_repository.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $IOS_APP_COMMIT_SHA 
    - cd tmp/$GITHUB_REPOSITORY_NAME
  allow_failure:
    exit_codes: 81680085

prepare_alpha_environment:
  extends: .common_alpha
  stage: prepare-alpha
  script: ./prepare_build_environment.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $ALPHA_BRANCH_TO_BUILD
  artifacts:
    reports:
      dotenv: .env
  when: manual    

test_alpha_ios:
  extends: .common_alpha_ios
  stage: test-alpha
  needs: [prepare_alpha_environment]  
  script:
    - bundle install
    - cd "$PATH_TO_APP_SOURCES"
    - bundle exec pod cache clean --all
    - bundle exec pod install --repo-update
    - bundle exec fastlane ios test
    - bundle exec fastlane test_ui

build_alpha:
  extends: .common_alpha_ios
  stage: build-alpha
  needs: [prepare_alpha_environment]  
  script:
    - pwd
    - bundle install
    - cd "$PATH_TO_APP_SOURCES"
    - bundle exec pod cache clean --all
    - bundle exec pod install --repo-update
    - bundle exec fastlane alpha commitHash:$IOS_APP_COMMIT_SHA issueNumber:"$IOS_ISSUE_NUMBER" # IOS_APP_COMMIT_SHA defined in prepare_alpha_environment phase script
  artifacts:
    expire_in: 1 week
    paths:
      - $PATH_TO_IPA
      - $PATH_TO_ZIP

# -------------------
# Beta releases
# develop branch to build nightly with dedicated tags

.common_beta:
  tags:
    - xcode16 # Must match of course a registered and enabled runner with this label and Xcode 16 installed
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule" # Only scheduled pipeline needed

.common_beta_ios:
  extends: .common_beta
  before_script:
    # Job fails with allowed error code if IOS_APP_COMMIT_SHA environment variable does not exist.
    # This IOS_APP_COMMIT_SHA variable is defined as environement variable in prepare-build-environment.sh
    - if [[ -z "$IOS_APP_COMMIT_SHA" ]]; then exit 81680085; fi
    - ./download_github_repository.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $IOS_APP_COMMIT_SHA 
    - cd tmp/$GITHUB_REPOSITORY_NAME
  allow_failure:
    exit_codes: 81680085

prepare_beta_environment:
  extends: .common_beta
  stage: prepare-beta
  script: ./prepare_build_environment.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $BETA_BRANCH_TO_BUILD
  artifacts:
    reports:
      dotenv: .env
  
test_beta_ios:
  extends: .common_beta_ios
  stage: test-beta
  needs: [prepare_beta_environment]  
  script:
    - bundle install
    - cd "$PATH_TO_APP_SOURCES"
    - bundle exec pod cache clean --all
    - bundle exec pod install --repo-update
    - bundle exec fastlane ios test
    - bundle exec fastlane test_ui

build_beta_ios:
  extends: .common_beta_ios
  stage: build-beta
  needs: [prepare_beta_environment]  
  script:
    - bundle install
    - cd "$PATH_TO_APP_SOURCES"
    - bundle exec pod cache clean --all
    - bundle exec pod install --repo-update
    - bundle exec fastlane beta commitHash:$IOS_APP_COMMIT_SHA
    # Creates tags dedicated to the CI/CD builds and TestFlight uploads using some commit hash, e.g. the last commit hash.
    # Will use first characters of the hash, but it might not be enough accurate because some commits may start with same value.
  artifacts:
    expire_in: 1 week
    paths:
      - $PATH_TO_IPA
      - $PATH_TO_ZIP

# -------------------
# Production releases
# main branch to build on ask

.common_prod:
  tags:
    - xcode16 # Must match of course a registered and enabled runner with this label and Xcode 16 installed

.common_prod_ios:
  extends: .common_prod
  before_script:
    # Job fails with allowed error code if IOS_APP_COMMIT_SHA environment variable does not exist.
    # This IOS_APP_COMMIT_SHA variable is defined as environement variable in prepare-build-environment.sh
    - if [[ -z "$IOS_APP_COMMIT_SHA" ]]; then exit 81680085; fi
    - ./download_github_repository.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $IOS_APP_COMMIT_SHA 
    - cd tmp/$GITHUB_REPOSITORY_NAME
  allow_failure:
    exit_codes: 81680085

prepare_production_environment:
  extends: .common_prod
  stage: prepare-production
  script: ./prepare_build_environment.sh $GITHUB_ORGANIZATION_NAME $GITHUB_REPOSITORY_NAME $PRODUCTION_BRANCH_TO_BUILD
  artifacts:
    reports:
      dotenv: .env
  when: manual      
  
build_production:
  extends: .common_prod_ios
  stage: build-production
  needs: [prepare_production_environment]
  script:
    - bundle install
    - cd "$PATH_TO_APP_SOURCES"
    - bundle exec pod cache clean --all
    - bundle exec pod install
    - bundle exec fastlane prod upload:true
  artifacts:
    expire_in: 1 week
    paths:
      - $PATH_TO_IPA
      - $PATH_TO_ZIP
  when: manual

Prepare environement build Shell script

We use a script to prepare the workspace

#!/usr/bin/env bash
# Software Name: OUDS iOS
# SPDX-FileCopyrightText: Copyright (c) Orange SA
# SPDX-License-Identifier: MIT

set -euxo pipefail

# Exit codes
# ----------

EXIT_STATUS_MISSING_PREREQUISITES=100
EXIT_STATUS_UNDEFINED_ENV_VARIABLES=101
EXIT_STATUS_ERROR_MISSING_TAG_OR_BRANCH=102
EXIT_STATUS_ERROR_NO_ORGANIZATION=200
EXIT_STATUS_ERROR_NO_PROJECT=201
EXIT_STATUS_GITHUB_REQUEST_FAILED=300
EXIT_STATUS_NO_COMMITS=301

# Functions
# ---------

DisplayUsage(){
    echo " Usage: ./prepare_build_environement.sh orga_name repo_name tag_or_branch"
}

Assert(){
  env_var_name=$1
  env_var_value=$2
  if [[ -z $env_var_value ]]; then
    echo "❌ The environment variable '$env_var_name' is undefined"
    exit $EXIT_STATUS_UNDEFINED_ENV_VARIABLES
  else
    echo "✅ The environment variable '$env_var_name' is defined"
  fi
}

Check(){
  env_var_name=$1
  env_var_value=$2
  if [[ -z $env_var_value ]]; then
    echo "⚠️  The environment variable '$env_var_name' is undefined, are you aware of that?"
  else
    echo "✅ The environment variable '$env_var_name' is defined"
  fi
}

# Requirements
# ------------

REQUIREMENTS=(curl jq)  

for someCommand in ${REQUIREMENTS[@]}; do
    command -v "$someCommand" > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
      >&2 echo "❌ Required '$someCommand' is not installed"
      exit $EXIT_STATUS_MISSING_PREREQUISITES
    fi
done

# Parameters
# ----------

GITHUB_ORGA_NAME=$1
if [[ -z $GITHUB_ORGA_NAME ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_NO_ORGANIZATION
fi

GITHUB_REPO_NAME=$2
if [[ -z $GITHUB_REPO_NAME ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_NO_PROJECT
fi

TAG_OR_BRANCH=$3 # e.g. "main" for production, "develop" for beta, other branch name for alpha
if [[ -z $TAG_OR_BRANCH ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_MISSING_TAG_OR_BRANCH
fi

# Check main environment variables (defined in GitLab project settings)
# ---------------------------------------------------------------------

Assert "OUDS_APPLE_ISSUER_ID" "$OUDS_APPLE_ISSUER_ID"
Assert "OUDS_APPLE_KEY_CONTENT" "$OUDS_APPLE_KEY_CONTENT"
Assert "OUDS_DEVELOPER_BUNDLE_IDENTIFIER" "$OUDS_DEVELOPER_BUNDLE_IDENTIFIER"
Assert "OUDS_MATTERMOST_HOOK_URL" "$OUDS_MATTERMOST_HOOK_URL"
Assert "OUDS_MATTERMOST_HOOK_BOT_NAME" "$OUDS_MATTERMOST_HOOK_BOT_NAME"
Assert "OUDS_MATTERMOST_HOOK_BOT_ICON_URL" "$OUDS_MATTERMOST_HOOK_BOT_ICON_URL"
Assert "OUDS_FASTLANE_APPLE_ID" "$OUDS_FASTLANE_APPLE_ID"
Assert "OUDS_DEVELOPER_PORTAL_TEAM_ID" "$OUDS_DEVELOPER_PORTAL_TEAM_ID"
Assert "OUDS_APPLE_KEY_ID" "$OUDS_APPLE_KEY_ID"
Assert "GITHUB_ACCESS_TOKEN" "$GITHUB_ACCESS_TOKEN"

Check "IOS_ISSUE_NUMBER" "$IOS_ISSUE_NUMBER"

# Get last commit hash
# --------------------

> .env

echo "Preparing environment..."
echo "Tag or branch to pull sources from is: '$TAG_OR_BRANCH'"

headers=(-L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28")
commits=$(curl "${headers[@]}"  https://api.github.com/repos/$GITHUB_ORGA_NAME/$GITHUB_REPO_NAME/commits\?per_page\=100\&sha\=$TAG_OR_BRANCH)
release_commit_sha=$(echo $commits | jq -r 'try(first | .sha)')

if [[ -z $release_commit_sha ]]; then
  echo "❌ Could not find any commit in qualif '$TAG_OR_BRANCH' on GitHub '$GITHUB_REPO_NAME' repository."
  exit $EXIT_STATUS_NO_COMMITS
else 
  echo "✅ Release commit to use is '$release_commit_sha'"
fi

echo "IOS_APP_COMMIT_SHA=$release_commit_sha" >> .env # Store environment variables for GitLab jobs
IOS_APP_COMMIT_SHA="$release_commit_sha"
export IOS_APP_COMMIT_SHA

echo "✅ It seems all environment variables are defined, let's continue"

GitHub download Shell script

The fllowing script will download the source from GitHub with the workspace prepared by the previous script.

#!/usr/bin/env bash
# Software Name: OUDS iOS
# SPDX-FileCopyrightText: Copyright (c) Orange SA
# SPDX-License-Identifier: MIT

set -uxo pipefail

# Exit codes
# ----------

EXIT_STATUS_ERROR_NO_ORGANIZATION=1
EXIT_STATUS_ERROR_NO_PROJECT=2
EXIT_STATUS_ERROR_NO_SHA1=3
EXIT_STATUS_GITHUB_REQUEST_FAILED=4

# Utils
# ------

DisplayUsage(){
    echo " Usage: ./download_github_repository.sh orga_name repo_name commit_sha1"
}

# Parameters
# ----------

GITHUB_ORGA_NAME=$1
if [[ -z $GITHUB_ORGA_NAME ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_NO_ORGANIZATION
fi

GITHUB_REPO_NAME=$2
if [[ -z $GITHUB_REPO_NAME ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_NO_PROJECT
fi

COMMIT_SHA=$3
if [[ -z $COMMIT_SHA ]]; then
    DisplayUsage
    exit $EXIT_STATUS_ERROR_NO_SHA1
fi

# Business logic
# --------------

echo "Downloading $GITHUB_ORGA_NAME/$GITHUB_REPO_NAME repository at $COMMIT_SHA"

TMP_DIR_PATH="tmp"
if [ -d $TMP_DIR_PATH ]; then
    echo "Delete old temp directory"
    rm -rf $TMP_DIR_PATH
fi

# No need to clone the Git repository which can be quite heavy.
# Using also SSH implies to have proxy settings allowing this protocol and to use private key
# but some developers of OUDS iOS are GitHub organization admins, thus their private key are much to powerfull
# and their use is too hazardous

echo "Create new temp directory"
mkdir "$TMP_DIR_PATH"

ZIP_FILE_PATH="$TMP_DIR_PATH/$GITHUB_REPO_NAME.zip"
HEADERS=(-L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_ACCESS_TOKEN" -H "X-GitHub-Api-Version: 2022-11-28")
echo "Download version..."
curlReturn=$(curl "${HEADERS[@]}" "https://api.github.com/repos/"$GITHUB_ORGA_NAME"/"$GITHUB_REPO_NAME"/zipball/"$COMMIT_SHA"" --output "$ZIP_FILE_PATH" 2>&1)
if [ $? -ne 0 ] ; then
   echo "Error with GitHub request: '$curlReturn'"
   exit $EXIT_STATUS_GITHUB_REQUEST_FAILED
fi

echo "Unzip version"
yes | unzip "$ZIP_FILE_PATH" -d $TMP_DIR_PATH
echo "Unzip completed ($?)"

# Rename for future steps
echo "Moving items..."
mv $TMP_DIR_PATH/"$GITHUB_ORGA_NAME"-"$GITHUB_REPO_NAME"-* "$TMP_DIR_PATH/$GITHUB_REPO_NAME"

echo "✅ It seems the sources have been downloaded and extracted successfully!"

Note that the GITHUB_ACCESS_TOKEN mus be a fine grained personal access token with permissions read and write for contents, read only for metadata, and read and write for commit statuses and issues. Click on this hyperlink to create such token, however you may need to contact your GitHub organization admins for validation or help. For Orange-OpenSource, use the usual help address you should know.

How it works

Alpha builds

The alpha builds must be created using a manual trigger of our internal pipeline. It needs a branch to pull with sources to builds, and some issue(s) number(s). The last commit hash will be computed and used. A first step will prepare the build then a second step will build and upload the app.

The alpha build then will be uploaded automatically to TestFlight and available for a team defined in the fastlane/Appfile (here alpha-team).

Both our Mattermost hook and the Fastlane lanes produce details about the build like version, issues and build number.

There are also in the app some extra fields defined in the app Info.plist through Fastlane and GitLab CI showing the app version, its build number, the build type (DEBUG for local builds, ALPHA for alpha release, BETA for beta release, PROD for production release) and the build details (issues numbers). The display name will be modified too.

Beta builds

The beta builds are created with a scheduled pipeline. This is quite the same logic as alpha builds, but with Git tags associated to the builds on develop branch (one for the build prefixed by ci/, one for TestFlight upload prefixed by Test_Flight) with commit hash as suffix.

The beta build is automatically uploaded to TestFlight for a dedicated team (here beta-team).

The Mattermostt hook is also used, the app display name and the build details are updated too.

Production builds

The production builds are created with manual trigger of pipeline. It will be done on mai branch, in release configuration, and shipped to TestFlight.

About CI/CD with GitHub Actions

Because we host our library and demo app on GitHub, we need to be sure the code compiles and the tests pass befoire merging for example. We want also to be sure DCO is applied (even if not mandatory today) and if no secrets are leaked.

We use a GitHub Actions so as to define a workflow with some actions to build demo application and test the library. It will help use to ensure code on pull requests or being merged compiles and has all tests green. This workflow is defined in this YAML, and makes build, unit tests and UI tests. Keep in mind we may have some troubles with UI tests.

We have also a gitleaks workflow making some scans on the code to loook fo secrets leaks, defined in this YAML.

We use also two GitHub apps making controls on pull requests and defining wether or not prerequisites are filled or not. There is on control to check if PR template are all defined , and one if DCO is applied.