1+ # Inspired by https://www.expobuilder.app
12name : React Native CI/CD
23
34on :
89 # description: "Build type to run"
910 # options:
1011 # - dev
11- # - prod-apk
1212 # - prod-aab
1313 # - ios-dev
1414 # - ios-prod
2323 # - android
2424 # - ios
2525 # - all
26- workflow_call :
27- secrets :
28- EXPO_TOKEN :
29- required : true
30- EXPO_APPLE_ID :
31- required : true
32- EXPO_APPLE_PASSWORD :
33- required : true
34- EXPO_TEAM_ID :
35- required : true
36- GOOGLE_PLAY_SERVICE_ACCOUNT :
37- required : false
38- SLACK_WEBHOOK :
39- required : false
40- DISCORD_WEBHOOK :
41- required : false
42- GITHUB_TOKEN :
43- required : true
26+ workflow_call :
27+ secrets :
28+ EXPO_TOKEN :
29+ required : true
30+ EXPO_APPLE_ID :
31+ required : true
32+ EXPO_APPLE_PASSWORD :
33+ required : true
34+ EXPO_TEAM_ID :
35+ required : true
36+ GOOGLE_PLAY_SERVICE_ACCOUNT :
37+ required : false
38+ SLACK_WEBHOOK :
39+ required : false
40+ DISCORD_WEBHOOK :
41+ required : false
42+ GITHUB_TOKEN :
43+ required : true
4444
4545env :
4646 EXPO_TOKEN : ${{ secrets.EXPO_TOKEN }}
4747 EXPO_APPLE_ID : ${{ secrets.EXPO_APPLE_ID }}
4848 EXPO_APPLE_PASSWORD : ${{ secrets.EXPO_APPLE_PASSWORD }}
4949 EXPO_TEAM_ID : ${{ secrets.EXPO_TEAM_ID }}
50- # GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
5150 SLACK_WEBHOOK : ${{ secrets.SLACK_WEBHOOK }}
52- # DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
5351 NODE_OPTIONS : --openssl-legacy-provider
5452
5553jobs :
56- check-skip :
57- runs-on : ubuntu-latest
58- if : " !contains(github.event.head_commit.message, '[skip ci]')"
59- steps :
60- - name : Skip CI check
61- run : echo "Proceeding with workflow"
62-
63- test :
64- needs : check-skip
54+ build-android :
55+ if : (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch'
6556 runs-on : ubuntu-latest
6657 steps :
6758 - name : 🏗 Checkout repository
@@ -86,30 +77,66 @@ jobs:
8677 ${{ runner.os }}-yarn-
8778
8879 - name : 📦 Install dependencies
89- run : yarn install
80+ run : |
81+ yarn install
82+ yarn global add eas-cli@latest
9083
91- - name : 🧹 Run ESLint
92- run : yarn lint
84+ - name : 📱 Setup EAS build cache
85+ uses : actions/cache@v3
86+ with :
87+ path : ~/.eas-build-local
88+ key : ${{ runner.os }}-eas-build-local-${{ hashFiles('**/package.json') }}
89+ restore-keys : |
90+ ${{ runner.os }}-eas-build-local-
9391
94- - name : 📢 Notify test results
95- if : always()
96- uses : rtCamp/action-slack-notify@v2
92+ - name : 🔄 Verify EAS CLI installation
93+ run : |
94+ echo "EAS CLI version:"
95+ eas --version
96+
97+ - name : 📋 Fix package.json main entry (Linux)
98+ run : |
99+ if ! command -v jq &> /dev/null; then
100+ sudo apt-get update && sudo apt-get install -y jq
101+ fi
102+ cp package.json package.json.bak
103+ jq '.main = "node_modules/expo/AppEntry.js"' package.json > package.json.tmp && mv package.json.tmp package.json
104+ cat package.json | grep "main"
105+
106+ - name : Build Development APK
107+ if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'dev' || github.event_name == 'push' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
108+ run : |
109+ export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
110+ eas build --platform android --profile development --local --non-interactive --output=./app-dev.apk
97111 env :
98- SLACK_WEBHOOK : ${{ env.SLACK_WEBHOOK }}
99- SLACK_COLOR : ${{ job.status == 'success' && 'good' || 'danger' }}
100- SLACK_TITLE : Test Results
101- SLACK_MESSAGE : " Tests ${{ job.status == 'success' && 'passed ✅' || 'failed ❌' }}"
112+ NODE_ENV : development
113+
114+ - name : 📱 Build Production AAB
115+ if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'prod-aab' || github.event_name == 'push' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
116+ run : |
117+ export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
118+ eas build --platform android --profile production --local --non-interactive --output=./app-prod.aab
119+ env :
120+ NODE_ENV : production
102121
103- build-and-release :
104- needs : test
122+ - name : 🚀 Publish to Expo (optional)
123+ if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'publish-expo'
124+ run : |
125+ eas update --auto
126+
127+ - name : 📦 Upload Android artifacts
128+ uses : actions/upload-artifact@v4
129+ with :
130+ name : android-builds
131+ path : |
132+ ./app-dev.apk
133+ ./app-prod.aab
134+ if-no-files-found : ignore
135+ retention-days : 7
136+
137+ build-ios :
105138 if : (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch'
106- strategy :
107- matrix :
108- platform : [android]
109- include :
110- - platform : ios
111- runs-on : macos-latest
112- runs-on : ${{ matrix.platform == 'ios' && 'macos-latest' || 'ubuntu-latest' }}
139+ runs-on : macos-latest
113140 steps :
114141 - name : 🏗 Checkout repository
115142 uses : actions/checkout@v4
@@ -150,124 +177,71 @@ jobs:
150177 echo "EAS CLI version:"
151178 eas --version
152179
153- - name : 📋 Fix package.json main entry
180+ - name : 📋 Fix package.json main entry (macOS)
154181 run : |
155- # Check if jq is installed, if not install it
156182 if ! command -v jq &> /dev/null; then
157- echo "Installing jq..."
158- sudo apt-get update && sudo apt-get install -y jq
183+ brew update || true
184+ brew install jq || true
159185 fi
186+ cp package.json package.json.bak
187+ jq '.main = "node_modules/expo/AppEntry.js"' package.json > package.json.tmp && mv package.json.tmp package.json
188+ cat package.json | grep "main"
160189
161- # Fix the main entry in package.json
162- if [ -f ./package.json ]; then
163- # Create a backup
164- cp package.json package.json.bak
165- # Update the package.json
166- jq '.main = "node_modules/expo/AppEntry.js"' package.json > package.json.tmp && mv package.json.tmp package.json
167- echo "Updated package.json main entry"
168- cat package.json | grep "main"
169- else
170- echo "package.json not found"
171- exit 1
172- fi
173-
174- - name : 📋 Update metro.config.js for SVG support
175- run : |
176- if [ -f ./metro.config.js ]; then
177- echo "Creating backup of metro.config.js"
178- cp ./metro.config.js ./metro.config.js.backup
179- echo "Updating metro.config.js to CommonJS format"
180- cat > ./metro.config.js << 'EOFMARKER'
181- /* eslint-disable @typescript-eslint/no-var-requires */
182- const { getDefaultConfig } = require('expo/metro-config');
183-
184- const config = getDefaultConfig(__dirname);
185-
186- const { transformer, resolver } = config;
187-
188- config.transformer = {
189- ...transformer,
190- babelTransformerPath: require.resolve('react-native-svg-transformer/expo'),
191- };
192-
193- config.resolver = {
194- ...resolver,
195- assetExts: resolver.assetExts.filter(ext => ext !== 'svg'),
196- sourceExts: [...resolver.sourceExts, 'svg'],
197- };
198-
199- module.exports = config;
200- EOFMARKER
201- echo "metro.config.js updated to CommonJS format"
202- else
203- echo "metro.config.js not found"
204- fi
205-
206- - name : 📱 Build Development APK
207- if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'dev' || github.event_name == 'push' && (matrix.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
208- run : |
209- # Build with increased memory limit
210- export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
211- eas build --platform android --profile development --local --non-interactive --output=./app-dev.apk
212- env :
213- NODE_ENV : development
214-
215- - name : 📱 Build Production APK
216- if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'prod-apk' || github.event_name == 'push' && (matrix.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
217- run : |
218- export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
219- eas build --platform android --profile production-apk --local --non-interactive --output=./app-prod.apk
220- env :
221- NODE_ENV : production
222-
223- - name : 📱 Build Production AAB
224- if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'prod-aab' || github.event_name == 'push' && (matrix.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
225- run : |
226- export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
227- eas build --platform android --profile production --local --non-interactive --output=./app-prod.aab
228- env :
229- NODE_ENV : production
230190
231191 - name : 📱 Build iOS Development
232- if : (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'ios-dev') && (matrix. platform == 'ios ' || github.event.inputs.platform == 'all ' || github.event.inputs.platform == 'ios ')
192+ if : (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'ios-dev') && (github.event.inputs. platform == 'all ' || github.event.inputs.platform == 'ios ' || github.event_name == 'push ')
233193 run : |
234194 export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
235195 eas build --platform ios --profile development --local --non-interactive --output=./app-ios-dev.app
236196 env :
237197 NODE_ENV : development
238198
239199 - name : 📱 Build iOS Production
240- if : (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'ios-prod') && (matrix. platform == 'ios ' || github.event.inputs.platform == 'all ' || github.event.inputs.platform == 'ios ')
200+ if : (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'ios-prod') && (github.event.inputs. platform == 'all ' || github.event.inputs.platform == 'ios ' || github.event_name == 'push ')
241201 run : |
242202 export NODE_OPTIONS="--openssl-legacy-provider --max_old_space_size=4096"
243203 eas build --platform ios --profile production --local --non-interactive --output=./app-ios-prod.ipa
244204 env :
245205 NODE_ENV : production
246206
247- - name : 🚀 Publish to Expo
248- if : github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'publish-expo'
207+ - name : 📦 Zip iOS .app (development)
249208 run : |
250- eas update --auto
251- env :
252- EXPO_TOKEN : ${{ secrets.EXPO_TOKEN }}
253-
254- # - name: 🚀 Submit to Play Store
255- # if: (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'publish-stores') && (matrix.platform == 'android' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android')
256- # run: |
257- # eas submit -p android --path ./app-prod.aab --non-interactive
258- # env:
259- # EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
260- # GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
261-
262- # - name: 🚀 Submit to App Store
263- # if: (github.event.inputs.buildType == 'all' || github.event.inputs.buildType == 'publish-stores') && (matrix.platform == 'ios' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios')
264- # run: |
265- # eas submit -p ios --path ./app-ios-prod.ipa --non-interactive
266- # env:
267- # EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
268- # EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }}
269- # EXPO_APPLE_PASSWORD: ${{ secrets.EXPO_APPLE_PASSWORD }}
270- # EXPO_TEAM_ID: ${{ secrets.EXPO_TEAM_ID }}
209+ if [ -d "./app-ios-dev.app" ]; then
210+ ditto -c -k --sequesterRsrc --keepParent "./app-ios-dev.app" "./app-ios-dev.zip"
211+ fi
212+
213+ - name : 📦 Upload iOS artifacts
214+ uses : actions/upload-artifact@v4
215+ with :
216+ name : ios-builds
217+ path : |
218+ ./app-ios-dev.app
219+ ./app-ios-dev.zip
220+ ./app-ios-prod.ipa
221+ if-no-files-found : ignore
222+ retention-days : 7
223+
224+ create-release :
225+ needs : [build-android, build-ios]
226+ runs-on : ubuntu-latest
227+ if : always()
228+ steps :
229+ - name : 🏗 Checkout repository
230+ uses : actions/checkout@v4
231+
232+ - name : ⬇️ Download Android artifacts
233+ if : ${{ needs.build-android.result == 'success' }}
234+ uses : actions/download-artifact@v4
235+ with :
236+ name : android-builds
237+ path : ./dist
238+
239+ - name : ⬇️ Download iOS artifacts
240+ if : ${{ needs.build-ios.result == 'success' }}
241+ uses : actions/download-artifact@v4
242+ with :
243+ name : ios-builds
244+ path : ./dist
271245
272246 - name : 🏷️ Generate build information
273247 id : build-info
@@ -276,46 +250,34 @@ jobs:
276250 BUILD_NUMBER=$(date +%Y%m%d%H%M)
277251 echo "version=$VERSION" >> $GITHUB_OUTPUT
278252 echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
279- # Generate changelog from commit messages since last tag
280253 if git describe --tags --abbrev=0 > /dev/null 2>&1; then
281254 LAST_TAG=$(git describe --tags --abbrev=0)
282255 git log $LAST_TAG..HEAD --pretty=format:"- %s" > changelog.md
283256 else
284257 git log --pretty=format:"- %s" -n 10 > changelog.md
285258 fi
286259
287- - name : 📝 Create GitHub Release
288- uses : softprops/action-gh-release@v1
260+ - name : 📝 Create or Update GitHub Release
261+ uses : softprops/action-gh-release@v2
289262 with :
290263 draft : true
291264 name : " Release v${{ steps.build-info.outputs.version }}-${{ steps.build-info.outputs.build_number }}"
292265 tag_name : " v${{ steps.build-info.outputs.version }}-${{ steps.build-info.outputs.build_number }}"
293266 files : |
294- ./app-dev.apk
295- ./app-prod.apk
296- ./app-prod.aab
297- ./app-ios-dev.app
298- ./app-ios-prod.ipa
267+ ./dist/ app-dev.apk
268+ ./dist/ app-prod.aab
269+ ./dist/ app-ios-dev.zip
270+ ./dist/ app-ios-prod.ipa
271+ fail_on_unmatched_files : false
299272 body_path : changelog.md
300273 env :
301274 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
302- - name : 📦 Upload build artifacts to GitHub
303- uses : actions/upload-artifact@v4
304- with :
305- name : app-builds
306- path : |
307- ./app-dev.apk
308- ./app-prod.apk
309- ./app-prod.aab
310- ./app-ios-dev.app
311- ./app-ios-prod.ipa
312- retention-days : 7
313275
314- - name : 📢 Notify build completion
276+ - name : 📢 Notify release status
315277 if : always()
316278 uses : rtCamp/action-slack-notify@v2
317279 env :
318280 SLACK_WEBHOOK : ${{ env.SLACK_WEBHOOK }}
319281 SLACK_COLOR : ${{ job.status == 'success' && 'good' || 'danger' }}
320- SLACK_TITLE : Build Results
321- SLACK_MESSAGE : " Build ${{ job.status == 'success' && 'completed successfully ✅' || 'failed ❌' }}"
282+ SLACK_TITLE : Release Results
283+ SLACK_MESSAGE : " Release ${{ job.status == 'success' && 'created/updated ✅' || 'failed ❌' }}"
0 commit comments