Skip to content

Commit 2bcd74c

Browse files
authored
ci: split build artifacts and fix testflight upload for runway (#27766)
--- ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR makes two related CI improvements to support Runway and downstream artifact consumers: **1. Split artifact uploads into separate named artifacts** Previously, iOS device builds uploaded both the IPA and xcarchive under a single artifact name (`ios-<build_name>`), and Android release builds uploaded both APK and AAB under `android-<build_name>`. This made it impossible for tools like Runway to select a specific file type by artifact name. Each file type now gets its own distinctly-named artifact: - `ios-app-<build_name>` — iOS simulator `.app.zip` - `ios-ipa-<build_name>` — iOS device `.ipa` - `ios-xcarchive-<build_name>` — iOS `.xcarchive.zip` - `android-apk-<build_name>` — Android APK - `android-aab-<build_name>` — Android AAB **2. Zip xcarchive into a single file** `.xcarchive` is a directory, not a file. `rename-artifacts.js` now zips it with `ditto` into a `.xcarchive.zip` single-file artifact, which is what Runway and other upload tools expect. **3. Fix TestFlight Fastlane upload for Developer role API keys** When `distribute_external` is `false`, the Fastlane lane now sets `skip_submission: true`. Without this, `upload_to_testflight` makes TestFlight management API calls that require an App Manager (or higher) role — a Developer role API key (used for nightly builds) can upload binaries but cannot manage distribution. This fixes authentication failures on nightly uploads. **4. Downstream workflow updates** `nightly-build.yml` and `upload-to-testflight.yml` are updated to reference the new `ios-ipa-<build_name>` artifact name, and the redundant `"false"` positional argument for `distribute_external` is removed (now handled inside the Fastlane lane). ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** N/A — these are CI/build pipeline and Fastlane changes only. Verification requires triggering a full build workflow and confirming: 1. GitHub Actions artifacts are uploaded under the new split names. 2. Nightly builds successfully upload to TestFlight without authentication errors. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- Generated with the help of the pr-description AI skill --> Made with [Cursor](https://cursor.com) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes GitHub Actions artifact naming/packaging and TestFlight upload options, which can break downstream CI consumers and release uploads if misconfigured. > > **Overview** > **Build artifacts are now uploaded as separate, type-specific artifacts** instead of bundling multiple outputs under `ios-*` / `android-*`, enabling downstream tools to target `ios-app-*`, `ios-ipa-*`, `ios-xcarchive-*`, `android-apk-*`, and `android-aab-*` independently. > > **iOS device builds now zip `.xcarchive` directories** in `scripts/rename-artifacts.js` (outputting a `.xcarchive.zip`) and workflows are updated to upload/download the new artifact names (notably for nightly and manual TestFlight uploads). > > **TestFlight upload behavior is adjusted in `ios/fastlane/Fastfile`** so when `distribute_external` is false it sets `skip_submission: true`, avoiding App Store Connect management calls that require higher-privilege API keys; corresponding workflows remove the extra ` > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a6e0abf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 5bd79a8 commit 2bcd74c

5 files changed

Lines changed: 64 additions & 38 deletions

File tree

.github/workflows/build.yml

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -328,29 +328,35 @@ jobs:
328328
SCRIPT_NAME="build:${{ matrix.platform }}:${BUILD_NAME//-/:}"
329329
yarn "$SCRIPT_NAME"
330330
331-
# Rename build artifacts (also zips simulator .app and writes ios_deploy_path / ios_archive_path outputs)
331+
# Rename build artifacts (zips simulator .app and .xcarchive; writes ios_deploy_path / ios_archive_path / android_*_path outputs)
332332
- name: Rename ${{ matrix.platform }} artifacts
333333
if: success()
334334
id: rename
335335
run: node scripts/rename-artifacts.js ${{ matrix.platform }}
336336

337337
# Upload build artifacts (only if build succeeded)
338-
- name: Upload iOS simulator artifacts
338+
- name: Upload iOS simulator app
339339
if: matrix.platform == 'ios' && env.IS_SIM_BUILD == 'true'
340340
uses: actions/upload-artifact@v4
341341
with:
342-
name: ios-${{ inputs.build_name }}
342+
name: ios-app-${{ inputs.build_name }}
343343
path: ${{ steps.rename.outputs.ios_deploy_path }}
344344
if-no-files-found: error
345345

346-
- name: Upload iOS device artifacts
346+
- name: Upload iOS IPA
347347
if: matrix.platform == 'ios' && env.IS_SIM_BUILD != 'true'
348348
uses: actions/upload-artifact@v4
349349
with:
350-
name: ios-${{ inputs.build_name }}
351-
path: |
352-
${{ steps.rename.outputs.ios_deploy_path }}
353-
${{ steps.rename.outputs.ios_archive_path }}
350+
name: ios-ipa-${{ inputs.build_name }}
351+
path: ${{ steps.rename.outputs.ios_deploy_path }}
352+
if-no-files-found: error
353+
354+
- name: Upload iOS xcarchive
355+
if: matrix.platform == 'ios' && env.IS_SIM_BUILD != 'true'
356+
uses: actions/upload-artifact@v4
357+
with:
358+
name: ios-xcarchive-${{ inputs.build_name }}
359+
path: ${{ steps.rename.outputs.ios_archive_path }}
354360
if-no-files-found: error
355361

356362
- name: Upload iOS sourcemap
@@ -361,27 +367,32 @@ jobs:
361367
path: ${{ steps.rename.outputs.ios_sourcemap_path }}
362368
if-no-files-found: warn
363369

364-
# Dev builds (CONFIGURATION=Debug): APK only — mirrors Bitrise IS_DEV_BUILD behavior
365-
- name: Upload Android dev artifacts
370+
# Dev builds (CONFIGURATION=Debug): APK only
371+
- name: Upload Android APK (dev)
366372
if: matrix.platform == 'android' && env.CONFIGURATION == 'Debug'
367373
uses: actions/upload-artifact@v4
368374
with:
369-
name: android-${{ inputs.build_name }}
375+
name: android-apk-${{ inputs.build_name }}
376+
path: ${{ steps.rename.outputs.android_apk_path }}
377+
if-no-files-found: error
378+
379+
# Release builds: APK and AAB as separate artifacts
380+
- name: Upload Android APK (release)
381+
if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug'
382+
uses: actions/upload-artifact@v4
383+
with:
384+
name: android-apk-${{ inputs.build_name }}
370385
path: ${{ steps.rename.outputs.android_apk_path }}
371386
if-no-files-found: error
372387

373-
# Release builds: APK + AAB — mirrors Bitrise Deploy APK + Deploy AAB
374-
- name: Upload Android release artifacts
388+
- name: Upload Android AAB
375389
if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug'
376390
uses: actions/upload-artifact@v4
377391
with:
378-
name: android-${{ inputs.build_name }}
379-
path: |
380-
${{ steps.rename.outputs.android_apk_path }}
381-
${{ steps.rename.outputs.android_aab_path }}
392+
name: android-aab-${{ inputs.build_name }}
393+
path: ${{ steps.rename.outputs.android_aab_path }}
382394
if-no-files-found: error
383395

384-
# Non-Debug builds (prod, RC, beta, test, e2e, exp): sourcemaps — mirrors Bitrise Deploy Android Sourcemaps step
385396
- name: Upload Android sourcemaps
386397
if: matrix.platform == 'android' && env.CONFIGURATION != 'Debug'
387398
uses: actions/upload-artifact@v4

.github/workflows/nightly-build.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ jobs:
108108
working-directory: ios
109109
bundler-cache: true
110110

111-
- name: Download iOS build artifact
111+
- name: Download iOS IPA artifact
112112
uses: actions/download-artifact@v4
113113
with:
114-
name: ios-main-exp
114+
name: ios-ipa-main-exp
115115

116116
- name: Find IPA path
117117
id: ipa
@@ -141,8 +141,7 @@ jobs:
141141
"github_actions_main-exp" \
142142
"${{ github.ref_name }}" \
143143
"${{ steps.ipa.outputs.path }}" \
144-
"MetaMask BETA & Release Candidates" \
145-
"false"
144+
"MetaMask BETA & Release Candidates"
146145
147146
- name: Cleanup API Key
148147
if: always()
@@ -168,10 +167,10 @@ jobs:
168167
working-directory: ios
169168
bundler-cache: true
170169

171-
- name: Download iOS build artifact
170+
- name: Download iOS IPA artifact
172171
uses: actions/download-artifact@v4
173172
with:
174-
name: ios-main-rc
173+
name: ios-ipa-main-rc
175174

176175
- name: Find IPA path
177176
id: ipa
@@ -203,8 +202,7 @@ jobs:
203202
"github_actions_main-rc" \
204203
"${{ github.ref_name }}" \
205204
"${{ steps.ipa.outputs.path }}" \
206-
"MetaMask BETA & Release Candidates" \
207-
"false"
205+
"MetaMask BETA & Release Candidates"
208206
209207
- name: Cleanup API Key
210208
if: always()

.github/workflows/upload-to-testflight.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ jobs:
8989
working-directory: ios
9090
bundler-cache: true
9191

92-
- name: Download iOS build artifact
92+
- name: Download iOS IPA artifact
9393
uses: actions/download-artifact@v4
9494
with:
95-
name: ios-main-${{ inputs.environment || 'rc' }}
95+
name: ios-ipa-main-${{ inputs.environment || 'rc' }}
9696

9797
- name: Find IPA path
9898
id: ipa

ios/fastlane/Fastfile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,28 @@ platform :ios do
9393
key_filepath: api_key_key_filepath,
9494
)
9595

96-
# Upload to TestFlight with API key from app_store_connect_api_key action
97-
upload_to_testflight(
96+
# Upload to TestFlight with API key from app_store_connect_api_key action.
97+
# When distribute_external is false we skip_submission so the action only
98+
# uploads the IPA binary. This avoids TestFlight management API calls that
99+
# require an App Manager (or higher) role API key — a Developer role key
100+
# can upload builds but cannot manage distribution.
101+
testflight_options = {
98102
api_key: api_key,
99103
ipa: ipa_path,
100104
distribute_external: distribute_external,
101105
groups: groups,
102106
notify_external_testers: notify_external_testers,
103107
changelog: changelog_message
104-
)
108+
}
109+
110+
if distribute_external
111+
testflight_options[:distribute_external] = true
112+
testflight_options[:notify_external_testers] = true
113+
testflight_options[:groups] = groups
114+
else
115+
testflight_options[:skip_submission] = true
116+
end
117+
118+
upload_to_testflight(testflight_options)
105119
end
106120
end
107-

scripts/rename-artifacts.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,21 @@ function renameIos() {
247247
console.log(`✅ Sourcemap path: ${sourcemapPath}`);
248248
}
249249

250-
// Rename xcarchive (only for device builds)
250+
// Zip xcarchive into a single file (only for device builds)
251+
// .xcarchive is a directory; zipping it produces a clean single-file artifact
252+
// that Runway and other tools can match by extension.
251253
if (!isSimBuild) {
252254
const oldArchive = path.join(__dirname, `../ios/build/${appName}.xcarchive`);
253255
if (fs.existsSync(oldArchive)) {
254-
const newArchive = path.join(
256+
const archiveZip = path.join(
255257
__dirname,
256-
`../ios/build/${newBaseName}.xcarchive`,
258+
`../ios/build/${newBaseName}.xcarchive.zip`,
257259
);
258-
execSync(`cp -r "${oldArchive}" "${newArchive}"`);
259-
console.log(`✅ Renamed archive: ${newArchive}`);
260-
setGithubOutput('ios_archive_path', newArchive);
260+
execSync(
261+
`ditto -c -k --sequesterRsrc --keepParent "${oldArchive}" "${archiveZip}"`,
262+
);
263+
console.log(`✅ Zipped archive: ${archiveZip}`);
264+
setGithubOutput('ios_archive_path', archiveZip);
261265
} else {
262266
console.log(`⚠️ Archive not found: ${oldArchive}`);
263267
}

0 commit comments

Comments
 (0)