MySigner

GitLab CI

Automate your app deployment with GitLab CI/CD

GitLab CI

This guide shows you how to set up automated iOS and Android deployment using GitLab CI/CD with MySigner.


Prerequisites

  • MySigner account with API token
  • App Store Connect credential configured (for iOS)
  • Google Play credential configured (for Android)
  • GitLab repository with your mobile app

GitLab CI/CD Variables

Add these variables to your GitLab project:

  1. Go to SettingsCI/CDVariables
  2. Add the following variables:
Variable Required Type Description Protected Masked
MYSIGNER_API_TOKEN Yes Variable Your MySigner API token
MYSIGNER_ORG_ID Yes Variable Your organization ID (find in dashboard URL or mysigner status)
MYSIGNER_API_URL No Variable Your MySigner dashboard URL (defaults to https://mysigner.dev)
MYSIGNER_EMAIL No Variable The email associated with your MySigner account
Enable "Protected" for production variables so they're only available on protected branches.

Authentication in CI

The CLI automatically detects environment variables — no config file needed. GitLab CI/CD variables are automatically available as environment variables in every job, so just set them once and every mysigner command will work.


iOS Configuration

Create .gitlab-ci.yml for iOS TestFlight deployment:

yaml
stages:
  - build
  - deploy

.macos_runner:
  tags:
    - macos
    - xcode
  before_script:
    - gem install mysigner

# TestFlight Deployment
deploy_testflight:
  extends: .macos_runner
  stage: deploy
  script:
    - mysigner doctor
    - mysigner ship testflight --verbose
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"
  artifacts:
    paths:
      - "*.ipa"
    expire_in: 1 week

# App Store Deployment
deploy_appstore:
  extends: .macos_runner
  stage: deploy
  script:
    - mysigner ship appstore --verbose
  rules:
    - if: $CI_COMMIT_TAG
      when: manual

Android Configuration

For Android deployments:

yaml
stages:
  - build
  - deploy

.android_runner:
  image: eclipse-temurin:17-jdk
  tags:
    - docker
  before_script:
    - apt-get update -qq && apt-get install -y -qq ruby ruby-dev build-essential
    - gem install mysigner

# Internal Testing
deploy_internal:
  extends: .android_runner
  stage: deploy
  script:
    - mysigner ship internal --platform android --verbose
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  artifacts:
    paths:
      - "app/build/outputs/bundle/release/*.aab"
    expire_in: 1 week

# Production Deployment
deploy_production:
  extends: .android_runner
  stage: deploy
  script:
    - mysigner ship production --platform android --verbose
  rules:
    - if: $CI_COMMIT_TAG
      when: manual

Combined iOS & Android Pipeline

Full configuration for both platforms:

yaml
stages:
  - test
  - build
  - deploy

# ==========================================
# iOS Jobs (macOS Runner)
# ==========================================

.ios_base:
  tags:
    - macos
    - xcode
  before_script:
    - gem install mysigner

ios:testflight:
  extends: .ios_base
  stage: deploy
  script:
    - mysigner doctor
    - mysigner ship testflight --verbose
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"
  artifacts:
    paths:
      - "*.ipa"
    expire_in: 1 week

ios:appstore:
  extends: .ios_base
  stage: deploy
  script:
    - mysigner ship appstore --verbose
  rules:
    - if: $CI_COMMIT_TAG
      when: manual
  allow_failure: false

# ==========================================
# Android Jobs (Docker Runner)
# ==========================================

.android_base:
  image: eclipse-temurin:17-jdk
  tags:
    - docker
  before_script:
    - apt-get update -qq && apt-get install -y -qq ruby ruby-dev build-essential
    - gem install mysigner
  cache:
    key: gradle-cache
    paths:
      - .gradle/
      - android/.gradle/

android:internal:
  extends: .android_base
  stage: deploy
  script:
    - mysigner ship internal --platform android --verbose
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"
  artifacts:
    paths:
      - "app/build/outputs/bundle/release/*.aab"
    expire_in: 1 week

android:production:
  extends: .android_base
  stage: deploy
  script:
    - mysigner ship production --platform android --verbose
  rules:
    - if: $CI_COMMIT_TAG
      when: manual
  allow_failure: false

Self-Hosted macOS Runner

For iOS builds, you need a macOS runner. Set up a GitLab Runner on your Mac:

Install GitLab Runner

bash
brew install gitlab-runner

Register the Runner

bash
gitlab-runner register

Follow the prompts: 1. Enter your GitLab URL 2. Enter the registration token (from Settings → CI/CD → Runners) 3. Enter a description (e.g., "macOS Build Machine") 4. Enter tags: macos,xcode 5. Choose executor: shell

Start the Runner

bash
gitlab-runner install
gitlab-runner start

Caching

Speed up builds with caching:

iOS (CocoaPods)

yaml
ios:testflight:
  cache:
    key: cocoapods-$CI_COMMIT_REF_SLUG
    paths:
      - Pods/
      - .cocoapods/
  script:
    - pod install --deployment
    - mysigner ship testflight

Android (Gradle)

yaml
android:internal:
  cache:
    key: gradle-$CI_COMMIT_REF_SLUG
    paths:
      - .gradle/
      - ~/.gradle/caches/
      - ~/.gradle/wrapper/

Environment-Specific Deployments

Using GitLab Environments

yaml
deploy_testflight:
  stage: deploy
  environment:
    name: testflight
    url: https://appstoreconnect.apple.com/
  script:
    - mysigner ship testflight

deploy_production:
  stage: deploy
  environment:
    name: production
    url: https://apps.apple.com/app/your-app-id
  script:
    - mysigner ship appstore
  when: manual

Scheduled Pipelines

Set up nightly builds:

  1. Go to BuildPipeline schedules
  2. Create a new schedule
  3. Set the cron expression (e.g., 0 2 * * * for 2 AM daily)
  4. Set variables if needed
yaml
nightly_build:
  stage: build
  script:
    - mysigner ship testflight
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"

Merge Request Pipelines

Run validation on merge requests:

yaml
validate:
  stage: test
  script:
    - mysigner doctor
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Notifications

Slack Notifications

yaml
.notify_slack:
  after_script:
    - |
      if [ "$CI_JOB_STATUS" == "success" ]; then
        curl -X POST -H 'Content-type: application/json' \
          --data "{\"text\":\"✅ $CI_PROJECT_NAME deployed to $CI_ENVIRONMENT_NAME\"}" \
          $SLACK_WEBHOOK_URL
      fi

deploy_testflight:
  extends: .notify_slack
  # ... rest of config

Email Notifications

Configure in GitLab project settings under SettingsIntegrationsEmails on push.


Troubleshooting

"Runner not available"

No runner with matching tags.

Fix: 1. Check runner is online in SettingsCI/CDRunners 2. Verify tags match between job and runner 3. For shared runners, ensure they support your platform

"Permission denied" on macOS

Ruby gem installation issues.

Fix: yaml before_script: - export GEM_HOME="$HOME/.gem" - export PATH="$GEM_HOME/bin:$PATH" - gem install mysigner

Job timeout

Build takes too long.

Fix: yaml deploy_appstore: timeout: 2 hours # Increase timeout script: - mysigner ship appstore

Variable not available

Protected variable on unprotected branch.

Fix: 1. Make the branch protected, or 2. Uncheck "Protected" on the variable


Best Practices

Branch Strategy

Branch Deployment Track
develop Automatic TestFlight / Internal
main Automatic TestFlight / Beta
Tags Manual App Store / Production

Artifact Management

yaml
artifacts:
  paths:
    - "*.ipa"
    - "*.aab"
  expire_in: 2 weeks
  when: on_success

Security

  1. Use protected variables for sensitive data
  2. Limit protected branches
  3. Use masked variables for tokens
  4. Rotate API tokens regularly

Pricing note: the MySigner CLI is free on every plan tier — there's no per-pipeline / per-build CLI cost.