Migrating TestFlight CI From OMA Runner To OMA Portal API v2
This tutorial explains how to replace the old OMA TestFlight GitLab runner with direct OMA Portal API calls from your own project CI.
The old method used a dedicated OMA runner and a script such as:
script: - sh /Users/runner/gitlab_runner/supersign/gitlab_runner_utils/testflight.sh tags: - testflight
With the new method, your project builds the .xcarchive, uploads it to OMA Portal through the API, creates the TestFlight request, then optionally waits until the release reaches the expected status.
Old runner tutorial:
How to make your continuous integration more continuous
What Changes
You no longer need:
- a dedicated OMA TestFlight runner
testflight.sh- the old runner-side
project.ymlprocessing - manual runner registration with OMA team
You now need:
- an ODI application configured for API access
- OAuth2 client credentials
- an OMA Portal
apiKey - your OMA Portal application ID
- a CI job that calls OMA Portal API v2
- either your own implementation, or the provided Ruby script
Prerequisites
- Create an application in Orange Developer Inside: https://inside01.api.orange.com
Select the API Backbone service. - Open an Other Support Request in OMA Portal.
In the request, provide the ODI application ID that will call the OMA Portal API. OMA team must authorize this ODI application. - Get your OMA Portal application ID.
This is the{appId}used in API URLs:/applications/{appId}/... - Get your OMA Portal
apiKey.
API v2 still requires theapiKeyheader, in addition to OAuth2. - Configure GitLab CI variables under Settings > CI/CD > Variables.
Required variables:
OMA_APP_ID OMA_OAUTH_CLIENT_ID OMA_OAUTH_CLIENT_SECRET OMA_API_KEY
Recommended GitLab settings:
- mark secrets as Masked
- mark secrets as Hidden when available
- mark variables as Protected only if your pipeline runs on protected branches or protected tags
- do not store secrets in YAML files or source code
Important: if your branch is not protected and the variables are marked Protected, GitLab will not expose them to the pipeline.
API Base URLs
OMA Portal API v2 uses two different base URLs.
For most API calls, use the ODI gateway:
https://inside01.api.orange.com/oma-portal/v2
For artifact upload only, use direct OMA Portal URL:
https://oma-portal.orange.fr/oma/api/v2/external
This exception exists because binary upload through the ODI gateway may hit payload size limits.
OAuth token endpoint:
https://inside01.api.orange.com/oauth/v3/token
Authentication
First request an OAuth2 token using client credentials:
curl -u "$OMA_OAUTH_CLIENT_ID:$OMA_OAUTH_CLIENT_SECRET" \ -H "Accept: application/json" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=client_credentials" \ https://inside01.api.orange.com/oauth/v3/token
Then use both headers for OMA Portal API v2:
-H "Authorization: Bearer $ACCESS_TOKEN" -H "apiKey:$OMA_API_KEY"
API Flow
- Build the iOS project and produce an
.xcarchive. - Zip the
.xcarchive. - Upload the zip as an OMA artifact.
- Create a TestFlight request from this artifact.
- Optionally poll release status until the expected status is reached.
Artifact upload example:
curl -F "file=@archive.zip" \ "https://oma-portal.orange.fr/oma/api/v2/external/applications/$OMA_APP_ID/artifacts?store=app_store_ios" \ -H "apiKey:$OMA_API_KEY" \ -H "Authorization: Bearer $ACCESS_TOKEN"
TestFlight request example:
curl -F "requestType=type_testFlight" \ -F "store=app_store_ios" \ -F "securityAnalysis=true" \ -F "whatsNew=Bug fixes and improvements" \ -F "publicLink=lastLink" \ -F "csv=@testflight-testers.csv" \ "https://inside01.api.orange.com/oma-portal/v2/applications/$OMA_APP_ID/artifacts/$ARTIFACT_ID/testflight" \ -H "apiKey:$OMA_API_KEY" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Public Link Configuration
Common public link modes:
newPublicLinknewTestGroupexistingGrouplastLink
To create no public link, omit the publicLink field entirely.
lastLink is inferred by OMA Portal as the most recent public link that does not belong to a group.
Examples
Create a new public link:
distribution: publicLink: "newPublicLink" csv: "testflight-testers.csv"
Create a new TestFlight group:
distribution: publicLink: "newTestGroup" group: "test-1" csv: "testflight-testers.csv"
Reuse an existing group:
distribution: publicLink: "existingGroup" group: "test-1" csv: "testflight-testers.csv"
Reuse the last public link:
distribution: publicLink: "lastLink" csv: "testflight-testers.csv"
No public link:
distribution: csv: "testflight-testers.csv"
Tester CSV
If you need to add testers, provide a CSV file and send it as multipart field csv.
Example:
tester1@example.com tester2@example.com
Do not include secrets in this file.
Option 1: Implement API Calls Yourself
Projects can implement the API flow in any language.
Minimum required operations:
- call OAuth token endpoint
- zip the
.xcarchive - upload the artifact to direct OMA Portal URL
- read the returned artifact ID
- create TestFlight request through ODI gateway
- optionally poll
/applications/{appId}/releases/{releaseId}
This gives full control, but each project must maintain its own API client, error handling, proxy handling, retries, and status polling.
Option 2: Use The Ruby Helper Script
Projects may use the provided Ruby script instead of implementing the API flow manually.
Required files:
oma_testflight_upload.rb oma-testflight.yml testflight-testers.csv
Example oma-testflight.yml:
---
appId: "${OMA_APP_ID}"
artifact:
path: "archive/MyApp.xcarchive"
store: "app_store_ios"
testflight:
securityAnalysis: true
whatsNew: "Bug fixes and improvements"
waitForStatus: "waiting_store_approval"
distribution:
publicLink: "lastLink"
csv: "testflight-testers.csv"For a new group:
distribution: publicLink: "newTestGroup" group: "test-1" csv: "testflight-testers.csv"
If the iOS application contains extensions, declare them under testflight.extensions:
testflight:
extensions:
PlugIns/CallKitExtension.appex: "com.orange.example.CallKit"
PlugIns/WidgetsExtension.appex: "com.orange.example.Widgets"The script sends these values to OMA Portal as multipart fields where the field name is the exact extension path found in the artifact, and the field value is the production bundle identifier:
PlugIns/CallKitExtension.appex=com.orange.example.CallKit PlugIns/WidgetsExtension.appex=com.orange.example.Widgets
The extension path must match the path detected by OMA when the artifact is created, for example PlugIns/CallKitExtension.appex.
Run locally or in CI:
ruby oma_testflight_upload.rb oma-testflight.yml
The script expects:
- Ruby
curlzip- a built
.xcarchive - required GitLab CI variables available as environment variables
The script:
- expands
${VARIABLE}values from environment - validates required variables
- zips the
.xcarchive - uploads the artifact
- creates the TestFlight request
- logs the TestFlight request URL and multipart payload fields
- polls release status until
waitForStatus
Default target status:
waiting_store_approval
You can override it:
testflight: waitForStatus: "published"
GitLab CI Example
Example pipeline with one build job and one OMA TestFlight job:
stages:
- build
- testflight
build_project:
stage: build
tags:
- xcodebuild
script:
- xcodebuild archive
-project MyApp.xcodeproj
-scheme MyApp
-archivePath archive/MyApp.xcarchive
-configuration Release
artifacts:
paths:
- archive/MyApp.xcarchive
expire_in: 1 day
create_oma_testflight:
stage: testflight
tags:
- xcodebuild
needs:
- job: build_project
artifacts: true
script:
- ruby oma_testflight_upload.rb oma-testflight.yml
Migrating From Old project.yml
Old runner configuration was often stored in project.yml.
Old example:
--- uploadBitcode: false uploadSymbols: true stripSwiftSymbols: true bundleid: com.orange.App.Dev: com.orange.app testflight: - tester1@example.com - tester2@example.com xcarchive: MyApp.xcarchive whatsNew: "fixes crash at launch" feedbackEmail: contact@example.com description: "TestFlight build" marketingURL: "https://example.com" privacyPolicyURL: "https://example.com/privacy" demoAccountLogin: "demo" demoAccountPassword: "password"
New recommended config:
---
appId: "${OMA_APP_ID}"
artifact:
path: "archive/MyApp.xcarchive"
store: "app_store_ios"
testflight:
securityAnalysis: true
whatsNew: "fixes crash at launch"
feedbackEmail: "contact@example.com"
description: "TestFlight build"
marketingURL: "https://example.com"
privacyPolicyURL: "https://example.com/privacy"
demoAccountLogin: "demo"
demoAccountPassword: "password"
waitForStatus: "waiting_store_approval"
distribution:
publicLink: "newTestGroup"
group: "test-1"
csv: "testflight-testers.csv"Move old tester emails into testflight-testers.csv:
tester1@example.com tester2@example.com
Remove the old runner job:
script: - sh /Users/runner/gitlab_runner/supersign/gitlab_runner_utils/testflight.sh tags: - testflight
Replace it with the API job:
script: - ruby oma_testflight_upload.rb oma-testflight.yml
Status Polling
After creating the TestFlight request, the script can poll:
GET /applications/{appId}/releases/{releaseId}Default behavior is to wait until:
waiting_store_approval
Depending on OMA Portal automation and polling timing, the release may already be in a later status when the next poll runs. The helper script treats a later status as success when it has already passed the configured waitForStatus.
Status order used by the helper script:
omaSigning omaStoreSubmission waiting_store_approval published
For example, if waitForStatus is waiting_store_approval and the next poll sees published, the script exits successfully.
For fully automated flows where you expect publication, use:
testflight: waitForStatus: "published"
The job fails if the release reaches an error status before the target status.
Security Notes
Do not commit:
- OAuth client secret
- OMA API key
- access tokens
- private credentials
- proxy credentials
Use GitLab CI variables.
Recommended:
-
OMA_OAUTH_CLIENT_SECRET: Masked + Hidden -
OMA_API_KEY: Masked + Hidden -
OMA_OAUTH_CLIENT_ID: Masked if possible -
OMA_APP_ID: can be a normal variable, but keeping it as a CI variable is cleaner
Use Protected only when the pipeline runs on protected refs.
Checklist
For migration
- remove old TestFlight runner job
- add
oma_testflight_upload.rb - add
oma-testflight.yml - add optional
testflight-testers.csv - configure GitLab CI variables
- update
.gitlab-ci.yml - ensure build job exports an
.xcarchive - ensure TestFlight job receives the archive artifact
- run pipeline
- check OMA Portal release request
For a new project
- create ODI application
- request OMA Portal API authorization
- configure GitLab CI variables
- add build job
- add API TestFlight job
- choose direct API implementation or Ruby script
- define distribution mode
- test once with a small tester CSV or public link mode
Comments
0 comments
Please sign in to leave a comment.