-
Notifications
You must be signed in to change notification settings - Fork 2.9k
969 lines (913 loc) · 40.5 KB
/
deploy.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
name: Deploy code to staging or production
on:
push:
branches: [staging, production, andrew-mobile-deploy]
env:
SHOULD_DEPLOY_PRODUCTION: ${{ github.ref == 'refs/heads/production' }}
#concurrency:
# group: ${{ github.workflow }}-${{ github.ref }}
# cancel-in-progress: true
jobs:
validateActor:
runs-on: ubuntu-latest
timeout-minutes: 90
outputs:
IS_DEPLOYER: ${{ fromJSON(steps.isUserDeployer.outputs.IS_DEPLOYER) || github.actor == 'OSBotify' || github.actor == 'os-botify[bot]' }}
steps:
- name: Check if user is deployer
id: isUserDeployer
run: |
if gh api /orgs/Expensify/teams/mobile-deployers/memberships/${{ github.actor }} --silent; then
echo "IS_DEPLOYER=true" >> "$GITHUB_OUTPUT"
else
echo "IS_DEPLOYER=false" >> "$GITHUB_OUTPUT"
fi
env:
GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }}
prep:
needs: validateActor
if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }}
runs-on: ubuntu-latest
outputs:
APP_VERSION: ${{ steps.getAppVersion.outputs.VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.OS_BOTIFY_TOKEN }}
- name: Setup git for OSBotify
uses: ./.github/actions/composite/setupGitForOSBotifyApp
id: setupGitForOSBotify
with:
GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
- name: Get app version
id: getAppVersion
run: echo "VERSION=$(jq -r .version < package.json)" >> "$GITHUB_OUTPUT"
# - name: Create and push tag
# if: ${{ github.ref == 'refs/heads/staging' }}
# run: |
# git tag ${{ steps.getAppVersion.outputs.VERSION }}
# git push origin --tags
# Note: we're updating the checklist before running the deploys and assuming that it will succeed on at least one platform
# deployChecklist:
# name: Create or update deploy checklist
# uses: ./.github/workflows/createDeployChecklist.yml
# if: ${{ github.ref == 'refs/heads/staging' }}
# needs: prep
# secrets: inherit
#
# buildAndroid:
# name: Build Android app
# uses: ./.github/workflows/buildAndroid.yml
# if: ${{ github.ref == 'refs/heads/staging' }}
# needs: prep
# secrets: inherit
# with:
# type: release
# ref: staging
#
# uploadAndroid:
# name: Upload Android build to Google Play Store
# needs: buildAndroid
# runs-on: ubuntu-latest
# env:
# RUBYOPT: '-rostruct'
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Setup Ruby
# uses: ruby/[email protected]
# with:
# bundler-cache: true
#
# - name: Download Android build artifacts
# uses: actions/download-artifact@v4
# with:
# path: /tmp/artifacts
# pattern: android-artifact-*
# merge-multiple: true
#
# - name: Log downloaded artifact paths
# run: ls -R /tmp/artifacts
#
# - name: Decrypt json w/ Google Play credentials
# run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg
# working-directory: android/app
#
# - name: Upload Android app to Google Play
# run: bundle exec fastlane android upload_google_play_internal
# env:
# aabPath: /tmp/artifacts/${{ needs.buildAndroid.outputs.AAB_FILE_NAME }}
#
# - name: Upload Android build to Browser Stack
# run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/tmp/artifacts/${{ needs.buildAndroid.outputs.AAB_FILE_NAME }}"
# env:
# BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
#
# submitAndroid:
# name: Submit Android app for production review
# needs: prep
# if: ${{ github.ref == 'refs/heads/production' }}
# runs-on: ubuntu-latest
# env:
# RUBYOPT: '-rostruct'
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Setup Ruby
# uses: ruby/[email protected]
# with:
# bundler-cache: true
#
# - name: Get Android native version
# id: getAndroidVersion
# run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT"
#
# - name: Decrypt json w/ Google Play credentials
# run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg
# working-directory: android/app
#
# - name: Submit Android build for review
# run: bundle exec fastlane android upload_google_play_production
# env:
# VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
#
# - name: Warn deployers if Android production deploy failed
# if: ${{ failure() }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 Android production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the <https://play.google.com/console/u/0/developers/8765590895836334604/app/4973041797096886180/releases/overview|Google Play Store>. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
android_hybrid:
name: Build and deploy Android HybridApp
needs: prep
runs-on: ubuntu-latest-xl
# Only deploy HybridApp to staging
if: ${{ github.ref != 'refs/heads/production' }}
defaults:
run:
working-directory: Mobile-Expensify/react-native
env:
RUBYOPT: '-rostruct'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: 'Expensify/Mobile-Expensify'
submodules: true
fetch-depth: 0
path: 'Mobile-Expensify'
token: ${{ secrets.OS_BOTIFY_TOKEN }}
- name: Update submodule
run: |
git submodule update --init
# TODO: REMOVE THIS LINE
git checkout andrew-mobile-deploy
- name: Configure MapBox SDK
run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version-file: 'Mobile-Expensify/react-native/.nvmrc'
cache: npm
cache-dependency-path: 'Mobile-Expensify/react-native'
- name: Install node modules
run: |
npm install
cd .. && npm install
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '17'
- name: Setup Ruby
uses: ruby/[email protected]
with:
bundler-cache: true
working-directory: 'Mobile-Expensify/react-native'
- name: Install New Expensify Gems
run: bundle install
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Load files 1Password
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
op document get --output ./upload-key.keystore upload-key.keystore
op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json
# Copy the keystore to the Android directory for Fullstory
cp ./upload-key.keystore ../Android
- name: Load Android upload keystore credentials from 1Password
id: load-credentials
uses: 1password/load-secrets-action@v2
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD
ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS
ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD
- name: Get Android native version
id: getAndroidVersion
run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT"
- name: Build Android app
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: bundle exec fastlane android build_hybrid
env:
ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }}
ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }}
ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }}
- name: Debug paths
run: |
ls -laR .
- name: Upload Android app to Google Play
run: bundle exec fastlane android upload_google_play_internal_hybrid
env:
VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }}
- name: Upload Android build to Browser Stack
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@../Android/build/outputs/bundle/release/Expensify-release.aab"
env:
BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
- name: Upload Android sourcemaps artifact
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
uses: actions/upload-artifact@v4
with:
name: android-sourcemaps-artifact
path: ../Android/build/generated/sourcemaps/react/release/index.android.bundle.map
- name: Upload Android build artifact
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
uses: actions/upload-artifact@v4
with:
name: android-build-artifact
path: ../Android/build/outputs/bundle/release/Expensify-release.aab
- name: Set current App version in Env
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV"
# - name: Warn deployers if Android production deploy failed
# if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 Android production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the <https://play.google.com/console/u/0/developers/8765590895836334604/app/4973041797096886180/releases/overview|Google Play Store>. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
# desktop:
# name: Build and deploy Desktop
# needs: prep
# runs-on: macos-14-large
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Setup Node
# uses: ./.github/actions/composite/setupNode
#
# - name: Decrypt Developer ID Certificate
# run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg
# env:
# DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }}
#
# - name: Build desktop app
# run: |
# if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then
# npm run desktop-build
# else
# npm run desktop-build-staging
# fi
# env:
# CSC_LINK: ${{ secrets.CSC_LINK }}
# CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# APPLE_ID: ${{ secrets.APPLE_ID }}
# APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
# AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
# AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# GCP_GEOLOCATION_API_KEY: $${{ secrets.GCP_GEOLOCATION_API_KEY_PRODUCTION }}
#
# - name: Upload desktop sourcemaps artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'desktop-sourcemaps-artifact' || 'desktop-staging-sourcemaps-artifact' }}
# path: ./desktop/dist/www/merged-source-map.js.map
#
# - name: Upload desktop build artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'desktop-build-artifact' || 'desktop-staging-build-artifact' }}
# path: ./desktop-build/NewExpensify.dmg
#
# iOS:
# name: Build and deploy iOS
# needs: prep
# env:
# DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer
# runs-on: macos-13-xlarge
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Configure MapBox SDK
# run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
#
# - name: Setup Node
# id: setup-node
# uses: ./.github/actions/composite/setupNode
#
# - name: Setup Ruby
# uses: ruby/[email protected]
# with:
# bundler-cache: true
#
# - name: Cache Pod dependencies
# uses: actions/cache@v4
# id: pods-cache
# with:
# path: ios/Pods
# key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
#
# - name: Compare Podfile.lock and Manifest.lock
# id: compare-podfile-and-manifest
# run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
#
# - name: Install cocoapods
# uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
# if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true'
# with:
# timeout_minutes: 10
# max_attempts: 5
# command: scripts/pod-install.sh
#
# - name: Decrypt AppStore profile
# run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg
# env:
# LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
#
# - name: Decrypt AppStore Notification Service profile
# run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg
# env:
# LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
#
# - name: Decrypt certificate
# run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg
# env:
# LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
#
# - name: Decrypt App Store Connect API key
# run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg
# env:
# LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
#
# - name: Get iOS native version
# id: getIOSVersion
# run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT"
#
# - name: Build iOS release app
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: bundle exec fastlane ios build
#
# - name: Upload release build to TestFlight
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: bundle exec fastlane ios upload_testflight
# env:
# APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }}
# APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }}
# APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }}
# APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
#
# - name: Submit build for App Store review
# if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: bundle exec fastlane ios submit_for_review
# env:
# VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }}
#
# - name: Upload iOS build to Browser Stack
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa"
# env:
# BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
#
# - name: Upload iOS sourcemaps artifact
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# uses: actions/upload-artifact@v4
# with:
# name: ios-sourcemaps-artifact
# path: ./main.jsbundle.map
#
# - name: Upload iOS build artifact
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# uses: actions/upload-artifact@v4
# with:
# name: ios-build-artifact
# path: /Users/runner/work/App/App/New\ Expensify.ipa
#
# - name: Warn deployers if iOS production deploy failed
# if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 iOS production deploy failed. Please manually submit ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the <https://appstoreconnect.apple.com/apps/1530278510/appstore|App Store>. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
iOS_hybrid:
name: Build and deploy iOS HybridApp
needs: prep
runs-on: macos-13-xlarge
env:
DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer
# Only deploy HybridApp to staging
if: ${{ github.ref != 'refs/heads/production' }}
defaults:
run:
working-directory: Mobile-Expensify/react-native
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: 'Expensify/Mobile-Expensify'
submodules: true
fetch-depth: 0
path: 'Mobile-Expensify'
token: ${{ secrets.OS_BOTIFY_TOKEN }}
- name: Update submodule
run: |
git submodule update --init
# TODO: REMOVE THIS LINE
git checkout andrew-mobile-deploy
- name: Configure MapBox SDK
run: |
./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version-file: 'Mobile-Expensify/react-native/.nvmrc'
cache-dependency-path: 'Mobile-Expensify/react-native'
- name: Install node modules
run: |
npm install
cd .. && npm install
- name: Setup Ruby
uses: ruby/[email protected]
with:
bundler-cache: true
working-directory: 'Mobile-Expensify/react-native'
- name: Install New Expensify Gems
run: bundle install
- name: Cache Pod dependencies
uses: actions/cache@v4
id: pods-cache
with:
path: ios/Pods
key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }}
- name: Compare Podfile.lock and Manifest.lock
id: compare-podfile-and-manifest
run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT"
- name: Install cocoapods
uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847
if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true'
with:
timeout_minutes: 10
max_attempts: 5
command: cd Mobile-Expensify/iOS && pod install
- name: Install 1Password CLI
uses: 1password/install-cli-action@v1
- name: Load files 1Password
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
run: |
op document get --output ./OldApp_AppStore.mobileprovision OldApp_AppStore
op document get --output ./OldApp_AppStore_Share_Extension.mobileprovision OldApp_AppStore_Share_Extension
- name: Decrypt AppStore profile
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
- name: Decrypt AppStore Notification Service profile
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
- name: Decrypt certificate
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
- name: Decrypt App Store Connect API key
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg
env:
LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }}
- name: Set current App version in Env
run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV"
- name: Get iOS native version
id: getIOSVersion
run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT"
- name: Build iOS release app
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: bundle exec fastlane ios build_hybrid
- name: Debug paths
run: |
ls -laR .
- name: Upload release build to TestFlight
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: bundle exec fastlane ios upload_testflight_hybrid
env:
APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }}
APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }}
APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }}
APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
- name: Upload iOS build to Browser Stack
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@builds/${{ env.VERSION }}/${{ env.VERSION }}.ipa"
env:
BROWSERSTACK: ${{ secrets.BROWSERSTACK }}
VERSION: "$(cat ../app/config/config.json | jq -r '.meta.version')"
- name: Upload iOS sourcemaps artifact
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
uses: actions/upload-artifact@v4
with:
name: ios-sourcemaps-artifact
path: ./main.jsbundle.map
- name: Upload iOS build artifact
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
uses: actions/upload-artifact@v4
with:
name: ios-build-artifact
path: builds/"$(cat ../app/config/config.json | jq -r '.meta.version')"/"$(cat ../app/config/config.json | jq -r '.meta.version')".ipa
# - name: Warn deployers if iOS production deploy failed
# if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 iOS production deploy failed. Please manually submit ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the <https://appstoreconnect.apple.com/apps/1530278510/appstore|App Store>. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
# web:
# name: Build and deploy Web
# needs: prep
# runs-on: ubuntu-latest-xl
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Setup Node
# uses: ./.github/actions/composite/setupNode
#
# - name: Setup Cloudflare CLI
# run: pip3 install cloudflare==2.19.0
#
# - name: Configure AWS Credentials
# uses: aws-actions/configure-aws-credentials@v4
# with:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: us-east-1
#
# - name: Build web
# run: |
# if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then
# npm run build
# else
# npm run build-staging
# fi
#
# - name: Build storybook docs
# continue-on-error: true
# run: |
# if [[ ${{ env.SHOULD_DEPLOY_PRODUCTION }} == 'true' ]]; then
# npm run storybook-build
# else
# npm run storybook-build-staging
# fi
#
# - name: Deploy to S3
# run: |
# aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist ${{ env.S3_URL }}/
# aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{ env.S3_URL }}/.well-known/apple-app-site-association
# aws s3 cp --acl public-read --content-type 'application/json' --metadata-directive REPLACE ${{ env.S3_URL }}/.well-known/apple-app-site-association ${{env.S3_URL }}/apple-app-site-association
# env:
# S3_URL: s3://${{ env.SHOULD_DEPLOY_PRODUCTION != 'true' && 'staging-' || '' }}expensify-cash
#
# - name: Purge Cloudflare cache
# run: /home/runner/.local/bin/cli4 --verbose --delete hosts=["${{ env.SHOULD_DEPLOY_PRODUCTION != 'true' && 'staging.' || '' }}new.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache
# env:
# CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }}
#
# - name: Verify staging deploy
# if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: ./.github/scripts/verifyDeploy.sh staging ${{ needs.prep.outputs.APP_VERSION }}
#
# - name: Verify production deploy
# if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# run: ./.github/scripts/verifyDeploy.sh production ${{ needs.prep.outputs.APP_VERSION }}
#
# - name: Upload web sourcemaps artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'web' || 'web-staging' }}-sourcemaps-artifact
# path: ./dist/merged-source-map.js.map
#
# - name: Compress web build .tar.gz and .zip
# run: |
# tar -czvf webBuild.tar.gz dist
# zip -r webBuild.zip dist
#
# - name: Upload .tar.gz web build artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'web' || 'web-staging' }}-build-tar-gz-artifact
# path: ./webBuild.tar.gz
#
# - name: Upload .zip web build artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'web' || 'web-staging' }}-build-zip-artifact
# path: ./webBuild.zip
#
# postSlackMessageOnFailure:
# name: Post a Slack message when any platform fails to build or deploy
# runs-on: ubuntu-latest
# if: ${{ failure() }}
# needs: [buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web]
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Post Slack message on failure
# uses: ./.github/actions/composite/announceFailedWorkflowInSlack
# with:
# SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
#
# checkDeploymentSuccess:
# runs-on: ubuntu-latest
# outputs:
# IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }}
# IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }}
# needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web]
# if: ${{ always() }}
# steps:
# - name: Check deployment success on at least one platform
# id: checkDeploymentSuccessOnAtLeastOnePlatform
# run: |
# isAtLeastOnePlatformDeployed="false"
# if [ ${{ github.ref }} == 'refs/heads/production' ]; then
# if [ "${{ needs.submitAndroid.result }}" == "success" ]; then
# isAtLeastOnePlatformDeployed="true"
# fi
# else
# if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then
# isAtLeastOnePlatformDeployed="true"
# fi
# fi
#
# if [ "${{ needs.iOS.result }}" == "success" ] || \
# [ "${{ needs.iOS_hybrid.result }}" == "success" ] || \
# [ "${{ needs.android_hybrid.result }}" == "success" ] || \
# [ "${{ needs.desktop.result }}" == "success" ] || \
# [ "${{ needs.web.result }}" == "success" ]; then
# isAtLeastOnePlatformDeployed="true"
# fi
# echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED=$isAtLeastOnePlatformDeployed" >> "$GITHUB_OUTPUT"
# echo "IS_AT_LEAST_ONE_PLATFORM_DEPLOYED is $isAtLeastOnePlatformDeployed"
#
# - name: Check deployment success on all platforms
# id: checkDeploymentSuccessOnAllPlatforms
# run: |
# isAllPlatformsDeployed="false"
# if [ "${{ needs.iOS.result }}" == "success" ] && \
# [ "${{ needs.iOS_hybrid.result }}" == "success" ] && \
# [ "${{ needs.android_hybrid.result }}" == "success" ] && \
# [ "${{ needs.desktop.result }}" == "success" ] && \
# [ "${{ needs.web.result }}" == "success" ]; then
# isAllPlatformsDeployed="true"
# fi
#
# if [ ${{ github.ref }} == 'refs/heads/production' ]; then
# if [ "${{ needs.submitAndroid.result }}" != "success" ]; then
# isAllPlatformsDeployed="false"
# fi
# else
# if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then
# isAllPlatformsDeployed="false"
# fi
# fi
#
# echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT"
# echo "IS_ALL_PLATFORMS_DEPLOYED is $isAllPlatformsDeployed"
#
# createPrerelease:
# runs-on: ubuntu-latest
# if: ${{ always() && github.ref == 'refs/heads/staging' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
# needs: [prep, checkDeploymentSuccess]
# steps:
# - name: Download all workflow run artifacts
# uses: actions/download-artifact@v4
#
# - name: 🚀 Create prerelease 🚀
# run: |
# gh release create ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --title ${{ needs.prep.outputs.APP_VERSION }} --generate-notes --prerelease --target staging
# RETRIES=0
# MAX_RETRIES=10
# until [[ $(gh release view ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }}) || $RETRIES -ge $MAX_RETRIES ]]; do
# echo "release not found, retrying $((MAX_RETRIES - RETRIES++)) times"
# sleep 1
# done
# env:
# GITHUB_TOKEN: ${{ github.token }}
#
# - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
# continue-on-error: true
# run: |
# mv ./desktop-staging-sourcemaps-artifact/merged-source-map.js.map ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map
# mv ./web-staging-sourcemaps-artifact/merged-source-map.js.map ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map
#
# # TODO: Add HybridApp artifacts to the release
# - name: Upload artifacts to GitHub Release
# continue-on-error: true
# run: |
# gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \
# ./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./android-build-artifact/app-production-release.aab \
# ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./desktop-staging-build-artifact/NewExpensify.dmg#NewExpensifyStaging.dmg \
# ./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./ios-build-artifact/New\ Expensify.ipa \
# ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./web-staging-build-tar-gz-artifact/webBuild.tar.gz#stagingWebBuild.tar.gz \
# ./web-staging-build-zip-artifact/webBuild.zip#stagingWebBuild.zip
# env:
# GITHUB_TOKEN: ${{ github.token }}
#
# - name: Warn deployers if staging deploy failed
# if: ${{ failure() }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 NewDot staging deploy failed. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
#
# finalizeRelease:
# runs-on: ubuntu-latest
# if: ${{ always() && github.ref == 'refs/heads/production' && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
# needs: [prep, checkDeploymentSuccess]
# steps:
# - name: Download all workflow run artifacts
# uses: actions/download-artifact@v4
#
# - name: 🚀 Edit the release to be no longer a prerelease 🚀
# run: |
# LATEST_RELEASE="$(gh release list --repo ${{ github.repository }} --exclude-pre-releases --json tagName,isLatest --jq '.[] | select(.isLatest) | .tagName')"
# gh api --method POST /repos/Expensify/App/releases/generate-notes -f "tag_name=${{ needs.prep.outputs.APP_VERSION }}" -f "previous_tag_name=$LATEST_RELEASE" | jq -r '.body' >> releaseNotes.md
# gh release edit ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --prerelease=false --latest --notes-file releaseNotes.md
# env:
# GITHUB_TOKEN: ${{ github.token }}
#
# - name: Rename web and desktop sourcemaps artifacts before assets upload in order to have unique ReleaseAsset.name
# continue-on-error: true
# run: |
# mv ./desktop-sourcemaps-artifact/merged-source-map.js.map ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map
# mv ./web-sourcemaps-artifact/merged-source-map.js.map ./web-sourcemaps-artifact/web-merged-source-map.js.map
#
# # TODO: Add HybridApp artifacts to the release
# - name: Upload artifacts to GitHub Release
# continue-on-error: true
# run: |
# gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \
# ./desktop-sourcemaps-artifact/desktop-merged-source-map.js.map#desktop-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./desktop-build-artifact/NewExpensify.dmg \
# ./web-sourcemaps-artifact/web-merged-source-map.js.map#web-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \
# ./web-build-tar-gz-artifact/webBuild.tar.gz \
# ./web-build-zip-artifact/webBuild.zip
# env:
# GITHUB_TOKEN: ${{ github.token }}
#
# - name: Warn deployers if production deploy failed
# if: ${{ failure() }}
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: "#DB4545",
# pretext: `<!subteam^S4TJJ3PSL>`,
# text: `💥 NewDot production deploy failed. 💥`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
#
# postSlackMessageOnSuccess:
# name: Post a Slack message when all platforms deploy successfully
# runs-on: ubuntu-latest
# if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }}
# needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease]
# steps:
# - name: 'Announces the deploy in the #announce Slack room'
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#announce',
# attachments: [{
# color: 'good',
# text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> to ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'staging' }} 🎉️`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
#
# - name: 'Announces the deploy in the #deployer Slack room'
# uses: 8398a7/action-slack@v3
# with:
# status: custom
# custom_payload: |
# {
# channel: '#deployer',
# attachments: [{
# color: 'good',
# text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> to ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) && 'production' || 'staging' }} 🎉️`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
#
# - name: 'Announces a production deploy in the #expensify-open-source Slack room'
# uses: 8398a7/action-slack@v3
# if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
# with:
# status: custom
# custom_payload: |
# {
# channel: '#expensify-open-source',
# attachments: [{
# color: 'good',
# text: `🎉️ Successfully deployed ${process.env.AS_REPO} <https://github.com/Expensify/App/releases/tag/${{ needs.prep.outputs.APP_VERSION }}|${{ needs.prep.outputs.APP_VERSION }}> to production 🎉️`,
# }]
# }
# env:
# GITHUB_TOKEN: ${{ github.token }}
# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
#
# postGithubComments:
# uses: ./.github/workflows/postDeployComments.yml
# if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }}
# needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease]
# with:
# version: ${{ needs.prep.outputs.APP_VERSION }}
# env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }}
# android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }}
# android_hybrid: ${{ needs.android_hybrid.result }}
# ios: ${{ needs.iOS.result }}
# ios_hybrid: ${{ needs.iOS_hybrid.result }}
# web: ${{ needs.web.result }}
# desktop: ${{ needs.desktop.result }}