indraq_cli 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +41 -15
  3. package/cloud-api/.dockerignore +18 -18
  4. package/cloud-api/.env.example +14 -14
  5. package/cloud-api/Dockerfile +19 -19
  6. package/cloud-api/README.md +93 -93
  7. package/cloud-api/dist/index.js +265 -27
  8. package/cloud-api/node_modules/.package-lock.json +1 -1
  9. package/cloud-api/package-lock.json +2 -2
  10. package/cloud-api/package.json +1 -1
  11. package/cloud-api/sql/001_init.sql +8 -8
  12. package/cloud-api/sql/002_user_providers_and_project_resources.sql +40 -40
  13. package/cloud-api/sql/003_environment_infrastructure.sql +19 -19
  14. package/cloud-api/sql/004_managed_aws_credentials.sql +20 -20
  15. package/cloud-api/sql/005_project_members_and_resource_grants.sql +22 -22
  16. package/cloud-api/sql/006_catalog_identity_password_security.sql +96 -96
  17. package/cloud-api/sql/007_aws_access_key_rotation.sql +16 -16
  18. package/cloud-api/sql/008_provider_scopes.sql +91 -91
  19. package/cloud-api/sql/009_scope_user_secrets_and_project_lifecycle.sql +13 -0
  20. package/cloud-api/src/index.ts +164 -27
  21. package/cloud-api/tsconfig.json +18 -18
  22. package/dist/cli/help-content.d.ts.map +1 -1
  23. package/dist/cli/help-content.js +29 -7
  24. package/dist/cli/help-content.js.map +1 -1
  25. package/dist/modules/access/commands/access.command.js +30 -30
  26. package/dist/modules/aws/services/route53-lookup.service.js.map +1 -1
  27. package/dist/modules/cloud/commands/cloud.command.d.ts +4 -0
  28. package/dist/modules/cloud/commands/cloud.command.d.ts.map +1 -1
  29. package/dist/modules/cloud/commands/cloud.command.js +52 -1
  30. package/dist/modules/cloud/commands/cloud.command.js.map +1 -1
  31. package/dist/modules/cloud/index.d.ts.map +1 -1
  32. package/dist/modules/cloud/index.js +6 -3
  33. package/dist/modules/cloud/index.js.map +1 -1
  34. package/dist/modules/cloud/services/cloud-jenkins.service.js +71 -71
  35. package/dist/modules/cloud/services/user-provider.service.d.ts +2 -8
  36. package/dist/modules/cloud/services/user-provider.service.d.ts.map +1 -1
  37. package/dist/modules/cloud/services/user-provider.service.js +7 -35
  38. package/dist/modules/cloud/services/user-provider.service.js.map +1 -1
  39. package/dist/modules/deploy/commands/build.command.d.ts +1 -0
  40. package/dist/modules/deploy/commands/build.command.d.ts.map +1 -1
  41. package/dist/modules/deploy/commands/build.command.js +12 -5
  42. package/dist/modules/deploy/commands/build.command.js.map +1 -1
  43. package/dist/modules/deploy/index.d.ts.map +1 -1
  44. package/dist/modules/deploy/index.js +11 -4
  45. package/dist/modules/deploy/index.js.map +1 -1
  46. package/dist/modules/prebuild/commands/prebuild.command.d.ts +0 -4
  47. package/dist/modules/prebuild/commands/prebuild.command.d.ts.map +1 -1
  48. package/dist/modules/prebuild/commands/prebuild.command.js +34 -38
  49. package/dist/modules/prebuild/commands/prebuild.command.js.map +1 -1
  50. package/dist/modules/prebuild/index.d.ts.map +1 -1
  51. package/dist/modules/prebuild/index.js +4 -8
  52. package/dist/modules/prebuild/index.js.map +1 -1
  53. package/dist/modules/scaffold/services/docker-template.service.js +95 -95
  54. package/dist/modules/scopes/commands/scope.command.d.ts.map +1 -1
  55. package/dist/modules/scopes/commands/scope.command.js +3 -47
  56. package/dist/modules/scopes/commands/scope.command.js.map +1 -1
  57. package/dist/modules/scopes/services/scope-native.service.js +2 -2
  58. package/dist/modules/scopes/services/scope-native.service.js.map +1 -1
  59. package/dist/modules/users/commands/user.command.d.ts +1 -0
  60. package/dist/modules/users/commands/user.command.d.ts.map +1 -1
  61. package/dist/modules/users/commands/user.command.js +15 -1
  62. package/dist/modules/users/commands/user.command.js.map +1 -1
  63. package/dist/modules/users/index.d.ts.map +1 -1
  64. package/dist/modules/users/index.js +3 -2
  65. package/dist/modules/users/index.js.map +1 -1
  66. package/dist/modules/users/providers/jenkins-user.provider.js +95 -95
  67. package/dist/shared/config/config-files.js +7 -7
  68. package/dist/shared/jenkins/jenkins.service.d.ts.map +1 -1
  69. package/dist/shared/jenkins/jenkins.service.js +13 -8
  70. package/dist/shared/jenkins/jenkins.service.js.map +1 -1
  71. package/dist/shared/jenkins/role-strategy-groovy.js +84 -84
  72. package/docs/ARCHITECTURE.md +29 -29
  73. package/docs/CREATE-DEPLOYMENT-reference.groovy +1105 -1105
  74. package/package.json +9 -5
  75. package/templates/jenkins/CREATE-DEPLOYMENT.groovy +1105 -1105
  76. package/templates/jenkins/Jenkinsfile-Mobile-App +759 -759
  77. package/CHANGELOG_v2.1.0.md +0 -27
  78. package/UPGRADE_v2.0_to_v2.1.md +0 -68
  79. package/cloud-api/dist/index.js.map +0 -1
@@ -1,759 +1,759 @@
1
- pipeline {
2
- agent any
3
-
4
- options {
5
- timestamps()
6
- disableConcurrentBuilds()
7
- buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '30', artifactDaysToKeepStr: '30', artifactNumToKeepStr: '30'))
8
- skipDefaultCheckout(true)
9
- }
10
-
11
- environment {
12
- ANDROID_HOME = '/opt/android-sdk'
13
- ANDROID_SDK_ROOT = '/opt/android-sdk'
14
- MOBILE_BUILDER_HOME = '/opt/mobile-builder'
15
- CI = '1'
16
- BUILD_RETENTION_DAYS = '30'
17
- PATH = "/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/usr/local/bin:/usr/bin:/bin:${env.PATH}"
18
- }
19
-
20
- parameters {
21
- // Requires Jenkins "File Parameter" plugin (ID: file-parameters).
22
- // IndraQ CLI uploads both files through Jenkins buildWithParameters.
23
- stashedFile 'SOURCE_BUNDLE'
24
- stashedFile 'ENV_FILE'
25
-
26
- string(name: 'PROJECT_ID', defaultValue: '', description: 'Stable IndraQ project ID. Supplied by IndraQ CLI.')
27
- string(name: 'APP_NAME', defaultValue: '', description: 'Application name. Supplied by IndraQ CLI.')
28
- string(name: 'APP_VERSION', defaultValue: '0.1', description: 'Android versionName supplied by IndraQ CLI.')
29
- string(name: 'APP_VERSION_CODE', defaultValue: '1', description: 'Android versionCode supplied by IndraQ CLI.')
30
- string(name: 'SOURCE_SHA256', defaultValue: '', description: 'SHA-256 of the uploaded source archive.')
31
-
32
- choice(name: 'PROJECT_TYPE', choices: ['Expo', 'React Native CLI'], description: 'Mobile project type')
33
- choice(name: 'BUILD_ENVIRONMENT', choices: ['Development', 'Staging', 'Production'], description: 'Build environment')
34
- choice(name: 'BUILD_OUTPUT', choices: ['Development Client', 'Debug APK', 'Release APK', 'Release AAB'], description: 'Build output')
35
- choice(name: 'BUILD_PROFILE', choices: ['FAST', 'CLEAN', 'FULL_RESET'], description: 'Build profile')
36
-
37
- string(name: 'SOURCE_REVISION', defaultValue: 'local', description: 'Local Git revision if available.')
38
- booleanParam(name: 'SOURCE_DIRTY', defaultValue: false, description: 'True when the developer had uncommitted changes.')
39
- booleanParam(name: 'VERBOSE_BUILD', defaultValue: false, description: 'Enable Gradle --stacktrace and --info logs.')
40
- }
41
-
42
- stages {
43
- stage('Validate Request') {
44
- steps {
45
- script {
46
- if (!params.PROJECT_ID?.trim() || !(params.PROJECT_ID ==~ /[A-Za-z0-9._-]+/)) {
47
- error 'PROJECT_ID is missing or invalid.'
48
- }
49
- if (!params.APP_NAME?.trim() || params.APP_NAME.trim().length() > 80 || !(params.APP_NAME ==~ /[A-Za-z0-9][A-Za-z0-9 ._-]*/)) {
50
- error 'APP_NAME must be 1-80 characters using letters, numbers, spaces, dots, underscores, or hyphens.'
51
- }
52
- if (!params.APP_VERSION?.trim() || !(params.APP_VERSION ==~ /\d+(?:\.\d+)*/)) {
53
- error 'APP_VERSION must be numeric dotted text such as 0.1 or 1.2.3.'
54
- }
55
- if (!params.APP_VERSION_CODE?.trim() || !(params.APP_VERSION_CODE ==~ /\d+/)) {
56
- error 'APP_VERSION_CODE must be a positive integer.'
57
- }
58
- def parsedVersionCode = params.APP_VERSION_CODE.toLong()
59
- if (parsedVersionCode < 1 || parsedVersionCode > 2100000000L) {
60
- error 'APP_VERSION_CODE must be between 1 and 2100000000.'
61
- }
62
- if (!(params.SOURCE_SHA256 ==~ /[a-fA-F0-9]{64}/)) {
63
- error 'SOURCE_SHA256 must be a 64-character SHA-256 digest.'
64
- }
65
-
66
- env.APP_ROOT = "${env.MOBILE_BUILDER_HOME}/${params.PROJECT_ID}"
67
- env.SOURCE_DIR = "${env.WORKSPACE}/source"
68
- env.ARTIFACT_ROOT = "${env.APP_ROOT}/artifacts"
69
- env.LOG_ROOT = "${env.APP_ROOT}/logs"
70
- env.BUILD_ARTIFACT_DIR = "${env.ARTIFACT_ROOT}/build-${env.BUILD_NUMBER}"
71
- env.BUILD_LOG_FILE = "${env.BUILD_ARTIFACT_DIR}/build.log"
72
- env.BUILD_JSON_FILE = "${env.BUILD_ARTIFACT_DIR}/build.json"
73
-
74
- // Reuse the Jenkins machine's shared Gradle cache. The previous
75
- // Git-based mobile pipeline used the normal Jenkins HOME/.gradle
76
- // cache, so already-installed wrapper distributions are available
77
- // without downloading Gradle again for every IndraQ source snapshot.
78
- def serverHome = env.HOME?.trim() ?: '/var/jenkins_home'
79
- env.GRADLE_USER_HOME = "${serverHome}/.gradle"
80
-
81
- // npm / Expo caches remain project-scoped so FULL_RESET cannot
82
- // destroy caches used by unrelated Jenkins jobs.
83
- env.NPM_CONFIG_CACHE = "${env.APP_ROOT}/.npm-cache"
84
- env.EXPO_HOME = "${env.APP_ROOT}/.expo-home"
85
-
86
- env.NODE_ENV_VALUE = params.BUILD_ENVIRONMENT == 'Development' ? 'development' : 'production'
87
-
88
- if (params.PROJECT_TYPE == 'Expo') {
89
- if (params.BUILD_ENVIRONMENT == 'Development' && params.BUILD_OUTPUT != 'Development Client') {
90
- error 'Invalid combination: Expo Development only supports Development Client.'
91
- }
92
- if ((params.BUILD_ENVIRONMENT == 'Staging' || params.BUILD_ENVIRONMENT == 'Production') &&
93
- !(params.BUILD_OUTPUT in ['Release APK', 'Release AAB'])) {
94
- error "Invalid combination: Expo ${params.BUILD_ENVIRONMENT} only supports Release APK or Release AAB."
95
- }
96
- } else if (params.PROJECT_TYPE == 'React Native CLI') {
97
- if (params.BUILD_ENVIRONMENT == 'Development' && params.BUILD_OUTPUT != 'Debug APK') {
98
- error 'Invalid combination: React Native CLI Development only supports Debug APK.'
99
- }
100
- if ((params.BUILD_ENVIRONMENT == 'Staging' || params.BUILD_ENVIRONMENT == 'Production') &&
101
- !(params.BUILD_OUTPUT in ['Release APK', 'Release AAB'])) {
102
- error "Invalid combination: React Native CLI ${params.BUILD_ENVIRONMENT} only supports Release APK or Release AAB."
103
- }
104
- } else {
105
- error "Unknown PROJECT_TYPE: ${params.PROJECT_TYPE}"
106
- }
107
-
108
- switch (params.BUILD_OUTPUT) {
109
- case 'Development Client':
110
- env.GRADLE_TASK = 'assembleDebug'
111
- env.NEEDS_ENV_FILE = 'false'
112
- env.ARTIFACT_EXT = 'apk'
113
- env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/debug"
114
- env.ARTIFACT_FILE_PATTERN = '*.apk'
115
- break
116
- case 'Debug APK':
117
- env.GRADLE_TASK = 'assembleDebug'
118
- env.NEEDS_ENV_FILE = 'true'
119
- env.ARTIFACT_EXT = 'apk'
120
- env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/debug"
121
- env.ARTIFACT_FILE_PATTERN = '*.apk'
122
- break
123
- case 'Release APK':
124
- env.GRADLE_TASK = 'assembleRelease'
125
- env.NEEDS_ENV_FILE = 'true'
126
- env.ARTIFACT_EXT = 'apk'
127
- env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/release"
128
- env.ARTIFACT_FILE_PATTERN = '*.apk'
129
- break
130
- case 'Release AAB':
131
- env.GRADLE_TASK = 'bundleRelease'
132
- env.NEEDS_ENV_FILE = 'true'
133
- env.ARTIFACT_EXT = 'aab'
134
- env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/bundle/release"
135
- env.ARTIFACT_FILE_PATTERN = '*.aab'
136
- break
137
- default:
138
- error "Unknown BUILD_OUTPUT: ${params.BUILD_OUTPUT}"
139
- }
140
-
141
- echo '========== IndraQ Mobile Build Request =========='
142
- echo "App: ${params.APP_NAME}"
143
- echo "Project ID: ${params.PROJECT_ID}"
144
- echo "Version: ${params.APP_VERSION}"
145
- echo "versionCode: ${params.APP_VERSION_CODE}"
146
- echo "Project type: ${params.PROJECT_TYPE}"
147
- echo "Environment: ${params.BUILD_ENVIRONMENT}"
148
- echo "Output: ${params.BUILD_OUTPUT}"
149
- echo "Profile: ${params.BUILD_PROFILE}"
150
- echo "Source revision: ${params.SOURCE_REVISION}"
151
- echo "Source has local changes: ${params.SOURCE_DIRTY}"
152
- echo "Gradle task: ${env.GRADLE_TASK}"
153
- }
154
- }
155
- }
156
-
157
- stage('Prepare Folders') {
158
- steps {
159
- sh '''#!/usr/bin/env bash
160
- set -Eeuo pipefail
161
- mkdir -p "$APP_ROOT" "$ARTIFACT_ROOT" "$LOG_ROOT" "$BUILD_ARTIFACT_DIR" "$GRADLE_USER_HOME" "$NPM_CONFIG_CACHE" "$EXPO_HOME"
162
- rm -rf "$SOURCE_DIR"
163
- mkdir -p "$SOURCE_DIR"
164
- touch "$BUILD_LOG_FILE"
165
- '''
166
- }
167
- }
168
-
169
- stage('Receive Source Snapshot') {
170
- steps {
171
- dir("${env.SOURCE_DIR}") {
172
- unstash 'SOURCE_BUNDLE'
173
- sh '''#!/usr/bin/env bash
174
- set -Eeuo pipefail
175
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
176
-
177
- ACTUAL_SHA=$(sha256sum SOURCE_BUNDLE | awk '{print $1}')
178
- if [ "$ACTUAL_SHA" != "$SOURCE_SHA256" ]; then
179
- echo "Source bundle SHA-256 mismatch."
180
- echo "Expected: $SOURCE_SHA256"
181
- echo "Actual: $ACTUAL_SHA"
182
- exit 1
183
- fi
184
-
185
- if tar -tzf SOURCE_BUNDLE | grep -Eq '(^/|(^|/)[.][.](/|$))'; then
186
- echo "Unsafe path detected inside uploaded source archive."
187
- exit 1
188
- fi
189
-
190
- tar -xzf SOURCE_BUNDLE --strip-components=1
191
- rm -f SOURCE_BUNDLE
192
-
193
- echo "Source snapshot verified and extracted."
194
- '''
195
- }
196
- }
197
- }
198
-
199
- stage('Prepare Environment') {
200
- steps {
201
- dir("${env.SOURCE_DIR}") {
202
- unstash 'ENV_FILE'
203
- sh '''#!/usr/bin/env bash
204
- set -Eeuo pipefail
205
- set +x
206
-
207
- if [ "$NEEDS_ENV_FILE" = "true" ] && [ ! -s ENV_FILE ]; then
208
- echo "A non-empty root .env file is required for this build output."
209
- exit 1
210
- fi
211
-
212
- if [ -s ENV_FILE ]; then
213
- mv ENV_FILE .env
214
- chmod 600 .env || true
215
- echo "Root .env received. Values are hidden."
216
- else
217
- rm -f ENV_FILE
218
- echo "No root .env supplied; this build output does not require one."
219
- fi
220
- '''
221
- }
222
- }
223
- }
224
-
225
- stage('Cleanup Old Build Files') {
226
- steps {
227
- sh '''#!/usr/bin/env bash
228
- set -Eeuo pipefail
229
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
230
- RETENTION_DAYS="${BUILD_RETENTION_DAYS:-30}"
231
-
232
- if ! [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || [ "$RETENTION_DAYS" -lt 1 ]; then
233
- echo "Invalid BUILD_RETENTION_DAYS: $RETENTION_DAYS"
234
- exit 1
235
- fi
236
-
237
- if [ -d "$ARTIFACT_ROOT" ]; then
238
- while IFS= read -r -d '' OLD_BUILD_DIR; do
239
- [ "$OLD_BUILD_DIR" = "$BUILD_ARTIFACT_DIR" ] && continue
240
- if ! find "$OLD_BUILD_DIR" -type f -mtime "-$RETENTION_DAYS" -print -quit | grep -q .; then
241
- echo "Deleting old artifact directory: $OLD_BUILD_DIR"
242
- rm -rf -- "$OLD_BUILD_DIR"
243
- fi
244
- done < <(find "$ARTIFACT_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'build-*' -print0)
245
- fi
246
-
247
- echo "Cleanup completed."
248
- '''
249
- }
250
- }
251
-
252
- stage('FULL_RESET') {
253
- when {
254
- expression { params.BUILD_PROFILE == 'FULL_RESET' }
255
- }
256
- steps {
257
- sh '''#!/usr/bin/env bash
258
- set -Eeuo pipefail
259
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
260
-
261
- echo "FULL_RESET: clearing project-local npm/Expo caches and generated build state."
262
- echo "Shared Jenkins Gradle cache is intentionally preserved: $GRADLE_USER_HOME"
263
- rm -rf "$NPM_CONFIG_CACHE" "$EXPO_HOME"
264
- mkdir -p "$NPM_CONFIG_CACHE" "$EXPO_HOME"
265
-
266
- # The uploaded source tree is already fresh, but clear Android-generated state
267
- # so FULL_RESET remains stronger than CLEAN without deleting the server's
268
- # shared Gradle distributions/caches.
269
- rm -rf "$SOURCE_DIR/android/.gradle" "$SOURCE_DIR/android/build" "$SOURCE_DIR/android/app/build"
270
- '''
271
- }
272
- }
273
-
274
- stage('Environment Check') {
275
- steps {
276
- dir("${env.SOURCE_DIR}") {
277
- sh '''#!/usr/bin/env bash
278
- set -Eeuo pipefail
279
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
280
-
281
- echo "========== Build Machine =========="
282
- echo "JAVA:"
283
- java -version
284
- echo "NODE:"
285
- node -v
286
- echo "NPM:"
287
- npm -v
288
- echo "ANDROID SDK:"
289
- echo "ANDROID_HOME=$ANDROID_HOME"
290
- echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT"
291
- echo "GRADLE CACHE:"
292
- echo "GRADLE_USER_HOME=$GRADLE_USER_HOME"
293
- if command -v gradle >/dev/null 2>&1; then
294
- echo "SYSTEM GRADLE:"
295
- gradle --version | sed -n '1,8p'
296
- else
297
- echo "SYSTEM GRADLE: not in PATH (project wrapper will use the shared Jenkins Gradle cache)."
298
- fi
299
- echo "ANDROID CLI:"
300
- sdkmanager --version || true
301
- '''
302
- }
303
- }
304
- }
305
-
306
- stage('CLEAN') {
307
- when {
308
- expression { params.BUILD_PROFILE == 'CLEAN' }
309
- }
310
- steps {
311
- dir("${env.SOURCE_DIR}") {
312
- sh '''#!/usr/bin/env bash
313
- set -Eeuo pipefail
314
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
315
-
316
- rm -rf node_modules .gradle .metro .turbo dist build coverage
317
- if [ "$PROJECT_TYPE" = "Expo" ]; then
318
- rm -rf android ios .expo
319
- else
320
- rm -rf android/.gradle android/build android/app/build android/.cxx android/app/.cxx
321
- fi
322
-
323
- echo "Project-level CLEAN completed."
324
- '''
325
- }
326
- }
327
- }
328
-
329
- stage('Install Dependencies') {
330
- steps {
331
- dir("${env.SOURCE_DIR}") {
332
- sh '''#!/usr/bin/env bash
333
- set -Eeuo pipefail
334
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
335
-
336
- if [ ! -f package.json ]; then
337
- echo "package.json is missing from the uploaded project root."
338
- exit 1
339
- fi
340
-
341
- if [ "$BUILD_PROFILE" = "FAST" ]; then
342
- npm install --prefer-offline --no-audit
343
- elif [ -f package-lock.json ]; then
344
- npm ci --no-audit
345
- else
346
- npm install --no-audit
347
- fi
348
- '''
349
- }
350
- }
351
- }
352
-
353
- stage('Validate Project Type') {
354
- steps {
355
- dir("${env.SOURCE_DIR}") {
356
- sh '''#!/usr/bin/env bash
357
- set -Eeuo pipefail
358
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
359
-
360
- node <<'NODE'
361
- const fs = require('fs');
362
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
363
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
364
- const projectType = process.env.PROJECT_TYPE;
365
- if (projectType === 'Expo' && !deps.expo) throw new Error('Expo selected, but expo is missing from package.json.');
366
- if (projectType === 'React Native CLI' && !deps['react-native']) throw new Error('React Native CLI selected, but react-native is missing from package.json.');
367
- NODE
368
-
369
- if [ "$PROJECT_TYPE" = "React Native CLI" ]; then
370
- [ -d android ] || { echo "React Native CLI project must contain android/."; exit 1; }
371
- [ -f android/gradlew ] || { echo "android/gradlew not found."; exit 1; }
372
- fi
373
- '''
374
- }
375
- }
376
- }
377
-
378
- stage('Validate Development Client') {
379
- when {
380
- expression { params.PROJECT_TYPE == 'Expo' && params.BUILD_OUTPUT == 'Development Client' }
381
- }
382
- steps {
383
- dir("${env.SOURCE_DIR}") {
384
- sh '''#!/usr/bin/env bash
385
- set -Eeuo pipefail
386
- node <<'NODE'
387
- const fs = require('fs');
388
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
389
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
390
- if (!deps['expo-dev-client']) throw new Error('Development Client requires expo-dev-client. Run: npx expo install expo-dev-client');
391
- NODE
392
- '''
393
- }
394
- }
395
- }
396
-
397
- stage('Expo Prebuild') {
398
- when {
399
- expression { params.PROJECT_TYPE == 'Expo' }
400
- }
401
- steps {
402
- dir("${env.SOURCE_DIR}") {
403
- sh '''#!/usr/bin/env bash
404
- set -Eeuo pipefail
405
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
406
- if [ "$BUILD_PROFILE" = "FAST" ]; then
407
- NODE_ENV="$NODE_ENV_VALUE" CI=1 npx expo prebuild --platform android
408
- else
409
- NODE_ENV="$NODE_ENV_VALUE" CI=1 npx expo prebuild --platform android --clean
410
- fi
411
- '''
412
- }
413
- }
414
- }
415
-
416
- stage('Prepare Android Project') {
417
- steps {
418
- dir("${env.SOURCE_DIR}/android") {
419
- sh '''#!/usr/bin/env bash
420
- set -Eeuo pipefail
421
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
422
-
423
- [ -f gradlew ] || { echo "gradlew not found inside android/."; exit 1; }
424
- [ -f gradle/wrapper/gradle-wrapper.jar ] || { echo "gradle-wrapper.jar not found."; exit 1; }
425
- [ -f gradle/wrapper/gradle-wrapper.properties ] || { echo "gradle-wrapper.properties not found."; exit 1; }
426
-
427
- # The source snapshot can originate on Windows. A CRLF gradlew has a hidden
428
- # carriage return in its shebang and Linux reports:
429
- # ./gradlew: cannot execute: required file not found
430
- # Normalize only the wrapper script; source files themselves are left intact.
431
- sed -i 's/\r$//' gradlew
432
- chmod +x gradlew
433
-
434
- # The Jenkins machine supplies Java, Android SDK/NDK and the shared Gradle
435
- # installation/cache. Keep the project wrapper as the compatibility source of
436
- # truth, but prefer an already-installed server Gradle when it exactly matches
437
- # the wrapper version. If the wrapper ever has to download, give it enough time
438
- # for a real network rather than Gradle's short default timeout.
439
- echo "sdk.dir=$ANDROID_HOME" > local.properties
440
-
441
- WRAPPER_PROPS="gradle/wrapper/gradle-wrapper.properties"
442
- if grep -q '^networkTimeout=' "$WRAPPER_PROPS"; then
443
- sed -i 's/^networkTimeout=.*/networkTimeout=120000/' "$WRAPPER_PROPS"
444
- else
445
- printf '\nnetworkTimeout=120000\n' >> "$WRAPPER_PROPS"
446
- fi
447
-
448
- WRAPPER_URL=$(grep '^distributionUrl=' "$WRAPPER_PROPS" | tail -n 1 | cut -d= -f2-)
449
- WRAPPER_FILE=$(basename "$WRAPPER_URL")
450
- WRAPPER_VERSION=${WRAPPER_FILE#gradle-}
451
- WRAPPER_VERSION=${WRAPPER_VERSION%-bin.zip}
452
- WRAPPER_VERSION=${WRAPPER_VERSION%-all.zip}
453
- SYSTEM_GRADLE_VERSION=''
454
- if command -v gradle >/dev/null 2>&1; then
455
- SYSTEM_GRADLE_VERSION=$(gradle --version 2>/dev/null | awk '/^Gradle / { print $2; exit }')
456
- fi
457
-
458
- cat > .indraq-gradle <<'GRADLE_RUNNER'
459
- #!/usr/bin/env bash
460
- set -Eeuo pipefail
461
- WRAPPER_PROPS="gradle/wrapper/gradle-wrapper.properties"
462
- WRAPPER_URL=$(grep '^distributionUrl=' "$WRAPPER_PROPS" | tail -n 1 | cut -d= -f2-)
463
- WRAPPER_FILE=$(basename "$WRAPPER_URL")
464
- WRAPPER_VERSION=${WRAPPER_FILE#gradle-}
465
- WRAPPER_VERSION=${WRAPPER_VERSION%-bin.zip}
466
- WRAPPER_VERSION=${WRAPPER_VERSION%-all.zip}
467
- CACHED_GRADLE=''
468
- if [ -n "$WRAPPER_VERSION" ] && [ -d "${GRADLE_USER_HOME:-$HOME/.gradle}/wrapper/dists" ]; then
469
- CACHED_GRADLE=$(find "${GRADLE_USER_HOME:-$HOME/.gradle}/wrapper/dists" -type f -path "*/gradle-$WRAPPER_VERSION/bin/gradle" -print -quit 2>/dev/null || true)
470
- fi
471
-
472
- # Exact cached wrapper distribution is safest because it matches the project.
473
- if [ -n "$CACHED_GRADLE" ]; then
474
- exec bash ./gradlew "$@"
475
- fi
476
-
477
- # If the Jenkins host has Gradle installed, reuse it when it is the same major
478
- # line as the requested wrapper. This avoids an internet download while still
479
- # refusing obviously incompatible Gradle generations (for example 8.x vs 9.x).
480
- if command -v gradle >/dev/null 2>&1; then
481
- SYSTEM_VERSION=$(gradle --version 2>/dev/null | awk '/^Gradle / { print $2; exit }')
482
- WRAPPER_MAJOR=${WRAPPER_VERSION%%.*}
483
- SYSTEM_MAJOR=${SYSTEM_VERSION%%.*}
484
- if [ -n "$WRAPPER_MAJOR" ] && [ "$SYSTEM_MAJOR" = "$WRAPPER_MAJOR" ]; then
485
- if [ "$SYSTEM_VERSION" != "$WRAPPER_VERSION" ]; then
486
- echo "IndraQ: using server Gradle $SYSTEM_VERSION for wrapper request $WRAPPER_VERSION (same major version)." >&2
487
- fi
488
- exec gradle "$@"
489
- fi
490
- fi
491
-
492
- # Last resort: project wrapper. Its networkTimeout is raised to 120 seconds and
493
- # it uses the shared Jenkins Gradle cache, so any download happens at most once.
494
- exec bash ./gradlew "$@"
495
- GRADLE_RUNNER
496
- chmod +x .indraq-gradle
497
-
498
- CACHED_GRADLE=''
499
- if [ -n "$WRAPPER_VERSION" ] && [ -d "$GRADLE_USER_HOME/wrapper/dists" ]; then
500
- CACHED_GRADLE=$(find "$GRADLE_USER_HOME/wrapper/dists" -type f -path "*/gradle-$WRAPPER_VERSION/bin/gradle" -print -quit 2>/dev/null || true)
501
- fi
502
-
503
- echo "========== Android Build Toolchain =========="
504
- echo "Android SDK : $ANDROID_HOME"
505
- echo "Shared Gradle cache: $GRADLE_USER_HOME"
506
- echo "Wrapper shebang : $(head -n 1 gradlew)"
507
- echo "Wrapper Gradle : ${WRAPPER_VERSION:-unknown}"
508
- if [ -n "$SYSTEM_GRADLE_VERSION" ]; then
509
- echo "System Gradle : $SYSTEM_GRADLE_VERSION"
510
- else
511
- echo "System Gradle : not in PATH"
512
- fi
513
- if [ -n "$CACHED_GRADLE" ]; then
514
- echo "Cached wrapper : yes ($CACHED_GRADLE)"
515
- elif [ -n "$SYSTEM_GRADLE_VERSION" ] && [ "$SYSTEM_GRADLE_VERSION" = "$WRAPPER_VERSION" ]; then
516
- echo "Gradle source : matching server Gradle executable"
517
- else
518
- echo "Cached wrapper : not found; wrapper may download once (timeout raised to 120s)."
519
- fi
520
- if [ -d "$ANDROID_HOME/ndk" ]; then
521
- echo "Installed NDKs:"
522
- find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' | sort || true
523
- fi
524
- echo "============================================="
525
- '''
526
- }
527
- }
528
- }
529
-
530
- stage('Apply Android Version') {
531
- steps {
532
- dir("${env.SOURCE_DIR}") {
533
- sh '''#!/usr/bin/env bash
534
- set -Eeuo pipefail
535
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
536
-
537
- node <<'NODE'
538
- const fs = require('fs');
539
- const path = require('path');
540
- const version = process.env.APP_VERSION;
541
- const versionCode = process.env.APP_VERSION_CODE;
542
- const candidates = [
543
- path.join('android', 'app', 'build.gradle'),
544
- path.join('android', 'app', 'build.gradle.kts')
545
- ];
546
- const gradleFile = candidates.find((candidate) => fs.existsSync(candidate));
547
- if (!gradleFile) throw new Error('android/app/build.gradle or build.gradle.kts was not found.');
548
-
549
- const original = fs.readFileSync(gradleFile, 'utf8');
550
- let codeUpdated = false;
551
- let nameUpdated = false;
552
- const lines = original.split(/\\r?\\n/).map((line) => {
553
- if (!codeUpdated) {
554
- let match = line.match(/^(\\s*versionCode\\s*=\\s*).+$/);
555
- if (match) {
556
- codeUpdated = true;
557
- return `${match[1]}${versionCode}`;
558
- }
559
- match = line.match(/^(\\s*versionCode\\s+).+$/);
560
- if (match) {
561
- codeUpdated = true;
562
- return `${match[1]}${versionCode}`;
563
- }
564
- }
565
- if (!nameUpdated) {
566
- let match = line.match(/^(\\s*versionName\\s*=\\s*).+$/);
567
- if (match) {
568
- nameUpdated = true;
569
- return `${match[1]}"${version}"`;
570
- }
571
- match = line.match(/^(\\s*versionName\\s+).+$/);
572
- if (match) {
573
- nameUpdated = true;
574
- return `${match[1]}"${version}"`;
575
- }
576
- }
577
- return line;
578
- });
579
-
580
- if (!codeUpdated || !nameUpdated) {
581
- throw new Error(`Unable to apply Android version. versionCode updated=${codeUpdated}, versionName updated=${nameUpdated}. Check ${gradleFile}.`);
582
- }
583
- fs.writeFileSync(gradleFile, lines.join('\\n'));
584
- console.log(`Android version applied in ${gradleFile}: versionName=${version}, versionCode=${versionCode}`);
585
- NODE
586
- '''
587
- }
588
- }
589
- }
590
-
591
- stage('Gradle Clean') {
592
- when {
593
- expression { params.BUILD_PROFILE == 'CLEAN' || params.BUILD_PROFILE == 'FULL_RESET' }
594
- }
595
- steps {
596
- dir("${env.SOURCE_DIR}/android") {
597
- sh '''#!/usr/bin/env bash
598
- set -Eeuo pipefail
599
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
600
- bash ./.indraq-gradle --stop || true
601
- NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle clean --console=plain --no-build-cache
602
- '''
603
- }
604
- }
605
- }
606
-
607
- stage('Build Android') {
608
- steps {
609
- dir("${env.SOURCE_DIR}/android") {
610
- script {
611
- if (params.BUILD_PROFILE == 'FAST') {
612
- if (params.VERBOSE_BUILD) {
613
- sh '''#!/usr/bin/env bash
614
- set -Eeuo pipefail
615
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
616
- NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --stacktrace --info
617
- '''
618
- } else {
619
- sh '''#!/usr/bin/env bash
620
- set -Eeuo pipefail
621
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
622
- NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain
623
- '''
624
- }
625
- } else if (params.VERBOSE_BUILD) {
626
- sh '''#!/usr/bin/env bash
627
- set -Eeuo pipefail
628
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
629
- NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --no-build-cache --rerun-tasks --stacktrace --info
630
- '''
631
- } else {
632
- sh '''#!/usr/bin/env bash
633
- set -Eeuo pipefail
634
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
635
- NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --no-build-cache --rerun-tasks
636
- '''
637
- }
638
- }
639
- }
640
- }
641
- }
642
-
643
- stage('Generate Build Metadata') {
644
- steps {
645
- sh '''#!/usr/bin/env bash
646
- set -Eeuo pipefail
647
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
648
- BUILD_TIME=$(TZ=Asia/Kolkata date '+%Y-%m-%dT%H:%M:%S%z')
649
- cat > "$BUILD_JSON_FILE" <<JSON
650
- {
651
- "app": "$APP_NAME",
652
- "projectId": "$PROJECT_ID",
653
- "version": "$APP_VERSION",
654
- "versionCode": $APP_VERSION_CODE,
655
- "projectType": "$PROJECT_TYPE",
656
- "environment": "$BUILD_ENVIRONMENT",
657
- "output": "$BUILD_OUTPUT",
658
- "profile": "$BUILD_PROFILE",
659
- "buildNumber": "$BUILD_NUMBER",
660
- "sourceRevision": "$SOURCE_REVISION",
661
- "sourceDirty": "$SOURCE_DIRTY",
662
- "sourceSha256": "$SOURCE_SHA256",
663
- "buildTime": "$BUILD_TIME"
664
- }
665
- JSON
666
- cat "$BUILD_JSON_FILE"
667
- '''
668
- }
669
- }
670
-
671
- stage('Collect Artifacts') {
672
- steps {
673
- sh '''#!/usr/bin/env bash
674
- set -Eeuo pipefail
675
- exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
676
-
677
- rm -rf "$WORKSPACE/jenkins-artifacts"
678
- mkdir -p "$WORKSPACE/jenkins-artifacts"
679
-
680
- [ -d "$ARTIFACT_SEARCH_DIR" ] || { echo "Artifact directory does not exist: $ARTIFACT_SEARCH_DIR"; exit 1; }
681
- mapfile -t ARTIFACTS < <(find "$ARTIFACT_SEARCH_DIR" -maxdepth 1 -type f -name "$ARTIFACT_FILE_PATTERN" | sort)
682
- [ "${#ARTIFACTS[@]}" -gt 0 ] || { echo "No .$ARTIFACT_EXT artifact found."; exit 1; }
683
-
684
- PRIMARY_ARTIFACT="${ARTIFACTS[0]}"
685
- SAFE_APP_NAME=$(printf '%s' "$APP_NAME" | sed -E 's/[^A-Za-z0-9._-]+/_/g; s/^_+//; s/_+$//')
686
- [ -n "$SAFE_APP_NAME" ] || SAFE_APP_NAME="app"
687
- BUILD_STAMP=$(TZ=Asia/Kolkata date '+%Y-%m-%d_%H%M')
688
- PRIMARY_ARTIFACT_NAME="${SAFE_APP_NAME}_${APP_VERSION}_${APP_VERSION_CODE}_${BUILD_STAMP}.${ARTIFACT_EXT}"
689
-
690
- cp "$PRIMARY_ARTIFACT" "$WORKSPACE/jenkins-artifacts/$PRIMARY_ARTIFACT_NAME"
691
- cp "$PRIMARY_ARTIFACT" "$BUILD_ARTIFACT_DIR/$PRIMARY_ARTIFACT_NAME"
692
- cp "$BUILD_LOG_FILE" "$WORKSPACE/jenkins-artifacts/build.log"
693
- cp "$BUILD_JSON_FILE" "$WORKSPACE/jenkins-artifacts/build.json"
694
- echo "$PRIMARY_ARTIFACT_NAME" > "$WORKSPACE/jenkins-artifacts/primary-artifact-name.txt"
695
-
696
- echo "Primary artifact: $PRIMARY_ARTIFACT_NAME"
697
- echo "Artifact timestamp timezone: Asia/Kolkata (IST)"
698
- '''
699
- }
700
- }
701
-
702
- stage('Archive & Publish Download Link') {
703
- steps {
704
- script {
705
- archiveArtifacts(artifacts: 'jenkins-artifacts/**/*', allowEmptyArchive: false, fingerprint: true)
706
- env.PRIMARY_ARTIFACT_NAME = readFile("${env.WORKSPACE}/jenkins-artifacts/primary-artifact-name.txt").trim()
707
- env.PRIMARY_ARTIFACT_URL = "${env.BUILD_URL}artifact/jenkins-artifacts/${env.PRIMARY_ARTIFACT_NAME}"
708
- currentBuild.description = "Download: ${env.PRIMARY_ARTIFACT_NAME}"
709
- echo '=============================================='
710
- echo 'DOWNLOAD READY'
711
- echo "File: ${env.PRIMARY_ARTIFACT_NAME}"
712
- echo "URL: ${env.PRIMARY_ARTIFACT_URL}"
713
- echo '=============================================='
714
- }
715
- }
716
- }
717
- }
718
-
719
- post {
720
- always {
721
- sh '''#!/usr/bin/env bash
722
- set +e
723
- # Never leave the developer's environment file in the Jenkins workspace.
724
- if [ -n "${SOURCE_DIR:-}" ] && [ "$SOURCE_DIR" != "/" ]; then
725
- rm -f "$SOURCE_DIR/.env" "$SOURCE_DIR/ENV_FILE"
726
- fi
727
- if [ -n "${WORKSPACE:-}" ] && [ "$WORKSPACE" != "/" ]; then
728
- rm -f "$WORKSPACE/ENV_FILE" "$WORKSPACE/SOURCE_BUNDLE"
729
- fi
730
- '''
731
- }
732
- success {
733
- echo '=============================================='
734
- echo 'BUILD SUCCESSFUL'
735
- echo "App: ${params.APP_NAME}"
736
- echo "Version: ${params.APP_VERSION}"
737
- echo "versionCode: ${params.APP_VERSION_CODE}"
738
- echo "Environment: ${params.BUILD_ENVIRONMENT}"
739
- echo "Output: ${params.BUILD_OUTPUT}"
740
- echo "Profile: ${params.BUILD_PROFILE}"
741
- echo "File: ${env.PRIMARY_ARTIFACT_NAME}"
742
- echo "Download: ${env.PRIMARY_ARTIFACT_URL}"
743
- echo "Server backup: ${env.BUILD_ARTIFACT_DIR}"
744
- echo '=============================================='
745
- }
746
- failure {
747
- echo '=============================================='
748
- echo 'BUILD FAILED'
749
- echo "App: ${params.APP_NAME}"
750
- echo "Version: ${params.APP_VERSION}"
751
- echo "versionCode: ${params.APP_VERSION_CODE}"
752
- echo "Environment: ${params.BUILD_ENVIRONMENT}"
753
- echo "Output: ${params.BUILD_OUTPUT}"
754
- echo "Profile: ${params.BUILD_PROFILE}"
755
- echo 'Check console logs above.'
756
- echo '=============================================='
757
- }
758
- }
759
- }
1
+ pipeline {
2
+ agent any
3
+
4
+ options {
5
+ timestamps()
6
+ disableConcurrentBuilds()
7
+ buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '30', artifactDaysToKeepStr: '30', artifactNumToKeepStr: '30'))
8
+ skipDefaultCheckout(true)
9
+ }
10
+
11
+ environment {
12
+ ANDROID_HOME = '/opt/android-sdk'
13
+ ANDROID_SDK_ROOT = '/opt/android-sdk'
14
+ MOBILE_BUILDER_HOME = '/opt/mobile-builder'
15
+ CI = '1'
16
+ BUILD_RETENTION_DAYS = '30'
17
+ PATH = "/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/usr/local/bin:/usr/bin:/bin:${env.PATH}"
18
+ }
19
+
20
+ parameters {
21
+ // Requires Jenkins "File Parameter" plugin (ID: file-parameters).
22
+ // IndraQ CLI uploads both files through Jenkins buildWithParameters.
23
+ stashedFile 'SOURCE_BUNDLE'
24
+ stashedFile 'ENV_FILE'
25
+
26
+ string(name: 'PROJECT_ID', defaultValue: '', description: 'Stable IndraQ project ID. Supplied by IndraQ CLI.')
27
+ string(name: 'APP_NAME', defaultValue: '', description: 'Application name. Supplied by IndraQ CLI.')
28
+ string(name: 'APP_VERSION', defaultValue: '0.1', description: 'Android versionName supplied by IndraQ CLI.')
29
+ string(name: 'APP_VERSION_CODE', defaultValue: '1', description: 'Android versionCode supplied by IndraQ CLI.')
30
+ string(name: 'SOURCE_SHA256', defaultValue: '', description: 'SHA-256 of the uploaded source archive.')
31
+
32
+ choice(name: 'PROJECT_TYPE', choices: ['Expo', 'React Native CLI'], description: 'Mobile project type')
33
+ choice(name: 'BUILD_ENVIRONMENT', choices: ['Development', 'Staging', 'Production'], description: 'Build environment')
34
+ choice(name: 'BUILD_OUTPUT', choices: ['Development Client', 'Debug APK', 'Release APK', 'Release AAB'], description: 'Build output')
35
+ choice(name: 'BUILD_PROFILE', choices: ['FAST', 'CLEAN', 'FULL_RESET'], description: 'Build profile')
36
+
37
+ string(name: 'SOURCE_REVISION', defaultValue: 'local', description: 'Local Git revision if available.')
38
+ booleanParam(name: 'SOURCE_DIRTY', defaultValue: false, description: 'True when the developer had uncommitted changes.')
39
+ booleanParam(name: 'VERBOSE_BUILD', defaultValue: false, description: 'Enable Gradle --stacktrace and --info logs.')
40
+ }
41
+
42
+ stages {
43
+ stage('Validate Request') {
44
+ steps {
45
+ script {
46
+ if (!params.PROJECT_ID?.trim() || !(params.PROJECT_ID ==~ /[A-Za-z0-9._-]+/)) {
47
+ error 'PROJECT_ID is missing or invalid.'
48
+ }
49
+ if (!params.APP_NAME?.trim() || params.APP_NAME.trim().length() > 80 || !(params.APP_NAME ==~ /[A-Za-z0-9][A-Za-z0-9 ._-]*/)) {
50
+ error 'APP_NAME must be 1-80 characters using letters, numbers, spaces, dots, underscores, or hyphens.'
51
+ }
52
+ if (!params.APP_VERSION?.trim() || !(params.APP_VERSION ==~ /\d+(?:\.\d+)*/)) {
53
+ error 'APP_VERSION must be numeric dotted text such as 0.1 or 1.2.3.'
54
+ }
55
+ if (!params.APP_VERSION_CODE?.trim() || !(params.APP_VERSION_CODE ==~ /\d+/)) {
56
+ error 'APP_VERSION_CODE must be a positive integer.'
57
+ }
58
+ def parsedVersionCode = params.APP_VERSION_CODE.toLong()
59
+ if (parsedVersionCode < 1 || parsedVersionCode > 2100000000L) {
60
+ error 'APP_VERSION_CODE must be between 1 and 2100000000.'
61
+ }
62
+ if (!(params.SOURCE_SHA256 ==~ /[a-fA-F0-9]{64}/)) {
63
+ error 'SOURCE_SHA256 must be a 64-character SHA-256 digest.'
64
+ }
65
+
66
+ env.APP_ROOT = "${env.MOBILE_BUILDER_HOME}/${params.PROJECT_ID}"
67
+ env.SOURCE_DIR = "${env.WORKSPACE}/source"
68
+ env.ARTIFACT_ROOT = "${env.APP_ROOT}/artifacts"
69
+ env.LOG_ROOT = "${env.APP_ROOT}/logs"
70
+ env.BUILD_ARTIFACT_DIR = "${env.ARTIFACT_ROOT}/build-${env.BUILD_NUMBER}"
71
+ env.BUILD_LOG_FILE = "${env.BUILD_ARTIFACT_DIR}/build.log"
72
+ env.BUILD_JSON_FILE = "${env.BUILD_ARTIFACT_DIR}/build.json"
73
+
74
+ // Reuse the Jenkins machine's shared Gradle cache. The previous
75
+ // Git-based mobile pipeline used the normal Jenkins HOME/.gradle
76
+ // cache, so already-installed wrapper distributions are available
77
+ // without downloading Gradle again for every IndraQ source snapshot.
78
+ def serverHome = env.HOME?.trim() ?: '/var/jenkins_home'
79
+ env.GRADLE_USER_HOME = "${serverHome}/.gradle"
80
+
81
+ // npm / Expo caches remain project-scoped so FULL_RESET cannot
82
+ // destroy caches used by unrelated Jenkins jobs.
83
+ env.NPM_CONFIG_CACHE = "${env.APP_ROOT}/.npm-cache"
84
+ env.EXPO_HOME = "${env.APP_ROOT}/.expo-home"
85
+
86
+ env.NODE_ENV_VALUE = params.BUILD_ENVIRONMENT == 'Development' ? 'development' : 'production'
87
+
88
+ if (params.PROJECT_TYPE == 'Expo') {
89
+ if (params.BUILD_ENVIRONMENT == 'Development' && params.BUILD_OUTPUT != 'Development Client') {
90
+ error 'Invalid combination: Expo Development only supports Development Client.'
91
+ }
92
+ if ((params.BUILD_ENVIRONMENT == 'Staging' || params.BUILD_ENVIRONMENT == 'Production') &&
93
+ !(params.BUILD_OUTPUT in ['Release APK', 'Release AAB'])) {
94
+ error "Invalid combination: Expo ${params.BUILD_ENVIRONMENT} only supports Release APK or Release AAB."
95
+ }
96
+ } else if (params.PROJECT_TYPE == 'React Native CLI') {
97
+ if (params.BUILD_ENVIRONMENT == 'Development' && params.BUILD_OUTPUT != 'Debug APK') {
98
+ error 'Invalid combination: React Native CLI Development only supports Debug APK.'
99
+ }
100
+ if ((params.BUILD_ENVIRONMENT == 'Staging' || params.BUILD_ENVIRONMENT == 'Production') &&
101
+ !(params.BUILD_OUTPUT in ['Release APK', 'Release AAB'])) {
102
+ error "Invalid combination: React Native CLI ${params.BUILD_ENVIRONMENT} only supports Release APK or Release AAB."
103
+ }
104
+ } else {
105
+ error "Unknown PROJECT_TYPE: ${params.PROJECT_TYPE}"
106
+ }
107
+
108
+ switch (params.BUILD_OUTPUT) {
109
+ case 'Development Client':
110
+ env.GRADLE_TASK = 'assembleDebug'
111
+ env.NEEDS_ENV_FILE = 'false'
112
+ env.ARTIFACT_EXT = 'apk'
113
+ env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/debug"
114
+ env.ARTIFACT_FILE_PATTERN = '*.apk'
115
+ break
116
+ case 'Debug APK':
117
+ env.GRADLE_TASK = 'assembleDebug'
118
+ env.NEEDS_ENV_FILE = 'true'
119
+ env.ARTIFACT_EXT = 'apk'
120
+ env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/debug"
121
+ env.ARTIFACT_FILE_PATTERN = '*.apk'
122
+ break
123
+ case 'Release APK':
124
+ env.GRADLE_TASK = 'assembleRelease'
125
+ env.NEEDS_ENV_FILE = 'true'
126
+ env.ARTIFACT_EXT = 'apk'
127
+ env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/apk/release"
128
+ env.ARTIFACT_FILE_PATTERN = '*.apk'
129
+ break
130
+ case 'Release AAB':
131
+ env.GRADLE_TASK = 'bundleRelease'
132
+ env.NEEDS_ENV_FILE = 'true'
133
+ env.ARTIFACT_EXT = 'aab'
134
+ env.ARTIFACT_SEARCH_DIR = "${env.SOURCE_DIR}/android/app/build/outputs/bundle/release"
135
+ env.ARTIFACT_FILE_PATTERN = '*.aab'
136
+ break
137
+ default:
138
+ error "Unknown BUILD_OUTPUT: ${params.BUILD_OUTPUT}"
139
+ }
140
+
141
+ echo '========== IndraQ Mobile Build Request =========='
142
+ echo "App: ${params.APP_NAME}"
143
+ echo "Project ID: ${params.PROJECT_ID}"
144
+ echo "Version: ${params.APP_VERSION}"
145
+ echo "versionCode: ${params.APP_VERSION_CODE}"
146
+ echo "Project type: ${params.PROJECT_TYPE}"
147
+ echo "Environment: ${params.BUILD_ENVIRONMENT}"
148
+ echo "Output: ${params.BUILD_OUTPUT}"
149
+ echo "Profile: ${params.BUILD_PROFILE}"
150
+ echo "Source revision: ${params.SOURCE_REVISION}"
151
+ echo "Source has local changes: ${params.SOURCE_DIRTY}"
152
+ echo "Gradle task: ${env.GRADLE_TASK}"
153
+ }
154
+ }
155
+ }
156
+
157
+ stage('Prepare Folders') {
158
+ steps {
159
+ sh '''#!/usr/bin/env bash
160
+ set -Eeuo pipefail
161
+ mkdir -p "$APP_ROOT" "$ARTIFACT_ROOT" "$LOG_ROOT" "$BUILD_ARTIFACT_DIR" "$GRADLE_USER_HOME" "$NPM_CONFIG_CACHE" "$EXPO_HOME"
162
+ rm -rf "$SOURCE_DIR"
163
+ mkdir -p "$SOURCE_DIR"
164
+ touch "$BUILD_LOG_FILE"
165
+ '''
166
+ }
167
+ }
168
+
169
+ stage('Receive Source Snapshot') {
170
+ steps {
171
+ dir("${env.SOURCE_DIR}") {
172
+ unstash 'SOURCE_BUNDLE'
173
+ sh '''#!/usr/bin/env bash
174
+ set -Eeuo pipefail
175
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
176
+
177
+ ACTUAL_SHA=$(sha256sum SOURCE_BUNDLE | awk '{print $1}')
178
+ if [ "$ACTUAL_SHA" != "$SOURCE_SHA256" ]; then
179
+ echo "Source bundle SHA-256 mismatch."
180
+ echo "Expected: $SOURCE_SHA256"
181
+ echo "Actual: $ACTUAL_SHA"
182
+ exit 1
183
+ fi
184
+
185
+ if tar -tzf SOURCE_BUNDLE | grep -Eq '(^/|(^|/)[.][.](/|$))'; then
186
+ echo "Unsafe path detected inside uploaded source archive."
187
+ exit 1
188
+ fi
189
+
190
+ tar -xzf SOURCE_BUNDLE --strip-components=1
191
+ rm -f SOURCE_BUNDLE
192
+
193
+ echo "Source snapshot verified and extracted."
194
+ '''
195
+ }
196
+ }
197
+ }
198
+
199
+ stage('Prepare Environment') {
200
+ steps {
201
+ dir("${env.SOURCE_DIR}") {
202
+ unstash 'ENV_FILE'
203
+ sh '''#!/usr/bin/env bash
204
+ set -Eeuo pipefail
205
+ set +x
206
+
207
+ if [ "$NEEDS_ENV_FILE" = "true" ] && [ ! -s ENV_FILE ]; then
208
+ echo "A non-empty root .env file is required for this build output."
209
+ exit 1
210
+ fi
211
+
212
+ if [ -s ENV_FILE ]; then
213
+ mv ENV_FILE .env
214
+ chmod 600 .env || true
215
+ echo "Root .env received. Values are hidden."
216
+ else
217
+ rm -f ENV_FILE
218
+ echo "No root .env supplied; this build output does not require one."
219
+ fi
220
+ '''
221
+ }
222
+ }
223
+ }
224
+
225
+ stage('Cleanup Old Build Files') {
226
+ steps {
227
+ sh '''#!/usr/bin/env bash
228
+ set -Eeuo pipefail
229
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
230
+ RETENTION_DAYS="${BUILD_RETENTION_DAYS:-30}"
231
+
232
+ if ! [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || [ "$RETENTION_DAYS" -lt 1 ]; then
233
+ echo "Invalid BUILD_RETENTION_DAYS: $RETENTION_DAYS"
234
+ exit 1
235
+ fi
236
+
237
+ if [ -d "$ARTIFACT_ROOT" ]; then
238
+ while IFS= read -r -d '' OLD_BUILD_DIR; do
239
+ [ "$OLD_BUILD_DIR" = "$BUILD_ARTIFACT_DIR" ] && continue
240
+ if ! find "$OLD_BUILD_DIR" -type f -mtime "-$RETENTION_DAYS" -print -quit | grep -q .; then
241
+ echo "Deleting old artifact directory: $OLD_BUILD_DIR"
242
+ rm -rf -- "$OLD_BUILD_DIR"
243
+ fi
244
+ done < <(find "$ARTIFACT_ROOT" -mindepth 1 -maxdepth 1 -type d -name 'build-*' -print0)
245
+ fi
246
+
247
+ echo "Cleanup completed."
248
+ '''
249
+ }
250
+ }
251
+
252
+ stage('FULL_RESET') {
253
+ when {
254
+ expression { params.BUILD_PROFILE == 'FULL_RESET' }
255
+ }
256
+ steps {
257
+ sh '''#!/usr/bin/env bash
258
+ set -Eeuo pipefail
259
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
260
+
261
+ echo "FULL_RESET: clearing project-local npm/Expo caches and generated build state."
262
+ echo "Shared Jenkins Gradle cache is intentionally preserved: $GRADLE_USER_HOME"
263
+ rm -rf "$NPM_CONFIG_CACHE" "$EXPO_HOME"
264
+ mkdir -p "$NPM_CONFIG_CACHE" "$EXPO_HOME"
265
+
266
+ # The uploaded source tree is already fresh, but clear Android-generated state
267
+ # so FULL_RESET remains stronger than CLEAN without deleting the server's
268
+ # shared Gradle distributions/caches.
269
+ rm -rf "$SOURCE_DIR/android/.gradle" "$SOURCE_DIR/android/build" "$SOURCE_DIR/android/app/build"
270
+ '''
271
+ }
272
+ }
273
+
274
+ stage('Environment Check') {
275
+ steps {
276
+ dir("${env.SOURCE_DIR}") {
277
+ sh '''#!/usr/bin/env bash
278
+ set -Eeuo pipefail
279
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
280
+
281
+ echo "========== Build Machine =========="
282
+ echo "JAVA:"
283
+ java -version
284
+ echo "NODE:"
285
+ node -v
286
+ echo "NPM:"
287
+ npm -v
288
+ echo "ANDROID SDK:"
289
+ echo "ANDROID_HOME=$ANDROID_HOME"
290
+ echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT"
291
+ echo "GRADLE CACHE:"
292
+ echo "GRADLE_USER_HOME=$GRADLE_USER_HOME"
293
+ if command -v gradle >/dev/null 2>&1; then
294
+ echo "SYSTEM GRADLE:"
295
+ gradle --version | sed -n '1,8p'
296
+ else
297
+ echo "SYSTEM GRADLE: not in PATH (project wrapper will use the shared Jenkins Gradle cache)."
298
+ fi
299
+ echo "ANDROID CLI:"
300
+ sdkmanager --version || true
301
+ '''
302
+ }
303
+ }
304
+ }
305
+
306
+ stage('CLEAN') {
307
+ when {
308
+ expression { params.BUILD_PROFILE == 'CLEAN' }
309
+ }
310
+ steps {
311
+ dir("${env.SOURCE_DIR}") {
312
+ sh '''#!/usr/bin/env bash
313
+ set -Eeuo pipefail
314
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
315
+
316
+ rm -rf node_modules .gradle .metro .turbo dist build coverage
317
+ if [ "$PROJECT_TYPE" = "Expo" ]; then
318
+ rm -rf android ios .expo
319
+ else
320
+ rm -rf android/.gradle android/build android/app/build android/.cxx android/app/.cxx
321
+ fi
322
+
323
+ echo "Project-level CLEAN completed."
324
+ '''
325
+ }
326
+ }
327
+ }
328
+
329
+ stage('Install Dependencies') {
330
+ steps {
331
+ dir("${env.SOURCE_DIR}") {
332
+ sh '''#!/usr/bin/env bash
333
+ set -Eeuo pipefail
334
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
335
+
336
+ if [ ! -f package.json ]; then
337
+ echo "package.json is missing from the uploaded project root."
338
+ exit 1
339
+ fi
340
+
341
+ if [ "$BUILD_PROFILE" = "FAST" ]; then
342
+ npm install --prefer-offline --no-audit
343
+ elif [ -f package-lock.json ]; then
344
+ npm ci --no-audit
345
+ else
346
+ npm install --no-audit
347
+ fi
348
+ '''
349
+ }
350
+ }
351
+ }
352
+
353
+ stage('Validate Project Type') {
354
+ steps {
355
+ dir("${env.SOURCE_DIR}") {
356
+ sh '''#!/usr/bin/env bash
357
+ set -Eeuo pipefail
358
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
359
+
360
+ node <<'NODE'
361
+ const fs = require('fs');
362
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
363
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
364
+ const projectType = process.env.PROJECT_TYPE;
365
+ if (projectType === 'Expo' && !deps.expo) throw new Error('Expo selected, but expo is missing from package.json.');
366
+ if (projectType === 'React Native CLI' && !deps['react-native']) throw new Error('React Native CLI selected, but react-native is missing from package.json.');
367
+ NODE
368
+
369
+ if [ "$PROJECT_TYPE" = "React Native CLI" ]; then
370
+ [ -d android ] || { echo "React Native CLI project must contain android/."; exit 1; }
371
+ [ -f android/gradlew ] || { echo "android/gradlew not found."; exit 1; }
372
+ fi
373
+ '''
374
+ }
375
+ }
376
+ }
377
+
378
+ stage('Validate Development Client') {
379
+ when {
380
+ expression { params.PROJECT_TYPE == 'Expo' && params.BUILD_OUTPUT == 'Development Client' }
381
+ }
382
+ steps {
383
+ dir("${env.SOURCE_DIR}") {
384
+ sh '''#!/usr/bin/env bash
385
+ set -Eeuo pipefail
386
+ node <<'NODE'
387
+ const fs = require('fs');
388
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
389
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
390
+ if (!deps['expo-dev-client']) throw new Error('Development Client requires expo-dev-client. Run: npx expo install expo-dev-client');
391
+ NODE
392
+ '''
393
+ }
394
+ }
395
+ }
396
+
397
+ stage('Expo Prebuild') {
398
+ when {
399
+ expression { params.PROJECT_TYPE == 'Expo' }
400
+ }
401
+ steps {
402
+ dir("${env.SOURCE_DIR}") {
403
+ sh '''#!/usr/bin/env bash
404
+ set -Eeuo pipefail
405
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
406
+ if [ "$BUILD_PROFILE" = "FAST" ]; then
407
+ NODE_ENV="$NODE_ENV_VALUE" CI=1 npx expo prebuild --platform android
408
+ else
409
+ NODE_ENV="$NODE_ENV_VALUE" CI=1 npx expo prebuild --platform android --clean
410
+ fi
411
+ '''
412
+ }
413
+ }
414
+ }
415
+
416
+ stage('Prepare Android Project') {
417
+ steps {
418
+ dir("${env.SOURCE_DIR}/android") {
419
+ sh '''#!/usr/bin/env bash
420
+ set -Eeuo pipefail
421
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
422
+
423
+ [ -f gradlew ] || { echo "gradlew not found inside android/."; exit 1; }
424
+ [ -f gradle/wrapper/gradle-wrapper.jar ] || { echo "gradle-wrapper.jar not found."; exit 1; }
425
+ [ -f gradle/wrapper/gradle-wrapper.properties ] || { echo "gradle-wrapper.properties not found."; exit 1; }
426
+
427
+ # The source snapshot can originate on Windows. A CRLF gradlew has a hidden
428
+ # carriage return in its shebang and Linux reports:
429
+ # ./gradlew: cannot execute: required file not found
430
+ # Normalize only the wrapper script; source files themselves are left intact.
431
+ sed -i 's/\r$//' gradlew
432
+ chmod +x gradlew
433
+
434
+ # The Jenkins machine supplies Java, Android SDK/NDK and the shared Gradle
435
+ # installation/cache. Keep the project wrapper as the compatibility source of
436
+ # truth, but prefer an already-installed server Gradle when it exactly matches
437
+ # the wrapper version. If the wrapper ever has to download, give it enough time
438
+ # for a real network rather than Gradle's short default timeout.
439
+ echo "sdk.dir=$ANDROID_HOME" > local.properties
440
+
441
+ WRAPPER_PROPS="gradle/wrapper/gradle-wrapper.properties"
442
+ if grep -q '^networkTimeout=' "$WRAPPER_PROPS"; then
443
+ sed -i 's/^networkTimeout=.*/networkTimeout=120000/' "$WRAPPER_PROPS"
444
+ else
445
+ printf '\nnetworkTimeout=120000\n' >> "$WRAPPER_PROPS"
446
+ fi
447
+
448
+ WRAPPER_URL=$(grep '^distributionUrl=' "$WRAPPER_PROPS" | tail -n 1 | cut -d= -f2-)
449
+ WRAPPER_FILE=$(basename "$WRAPPER_URL")
450
+ WRAPPER_VERSION=${WRAPPER_FILE#gradle-}
451
+ WRAPPER_VERSION=${WRAPPER_VERSION%-bin.zip}
452
+ WRAPPER_VERSION=${WRAPPER_VERSION%-all.zip}
453
+ SYSTEM_GRADLE_VERSION=''
454
+ if command -v gradle >/dev/null 2>&1; then
455
+ SYSTEM_GRADLE_VERSION=$(gradle --version 2>/dev/null | awk '/^Gradle / { print $2; exit }')
456
+ fi
457
+
458
+ cat > .indraq-gradle <<'GRADLE_RUNNER'
459
+ #!/usr/bin/env bash
460
+ set -Eeuo pipefail
461
+ WRAPPER_PROPS="gradle/wrapper/gradle-wrapper.properties"
462
+ WRAPPER_URL=$(grep '^distributionUrl=' "$WRAPPER_PROPS" | tail -n 1 | cut -d= -f2-)
463
+ WRAPPER_FILE=$(basename "$WRAPPER_URL")
464
+ WRAPPER_VERSION=${WRAPPER_FILE#gradle-}
465
+ WRAPPER_VERSION=${WRAPPER_VERSION%-bin.zip}
466
+ WRAPPER_VERSION=${WRAPPER_VERSION%-all.zip}
467
+ CACHED_GRADLE=''
468
+ if [ -n "$WRAPPER_VERSION" ] && [ -d "${GRADLE_USER_HOME:-$HOME/.gradle}/wrapper/dists" ]; then
469
+ CACHED_GRADLE=$(find "${GRADLE_USER_HOME:-$HOME/.gradle}/wrapper/dists" -type f -path "*/gradle-$WRAPPER_VERSION/bin/gradle" -print -quit 2>/dev/null || true)
470
+ fi
471
+
472
+ # Exact cached wrapper distribution is safest because it matches the project.
473
+ if [ -n "$CACHED_GRADLE" ]; then
474
+ exec bash ./gradlew "$@"
475
+ fi
476
+
477
+ # If the Jenkins host has Gradle installed, reuse it when it is the same major
478
+ # line as the requested wrapper. This avoids an internet download while still
479
+ # refusing obviously incompatible Gradle generations (for example 8.x vs 9.x).
480
+ if command -v gradle >/dev/null 2>&1; then
481
+ SYSTEM_VERSION=$(gradle --version 2>/dev/null | awk '/^Gradle / { print $2; exit }')
482
+ WRAPPER_MAJOR=${WRAPPER_VERSION%%.*}
483
+ SYSTEM_MAJOR=${SYSTEM_VERSION%%.*}
484
+ if [ -n "$WRAPPER_MAJOR" ] && [ "$SYSTEM_MAJOR" = "$WRAPPER_MAJOR" ]; then
485
+ if [ "$SYSTEM_VERSION" != "$WRAPPER_VERSION" ]; then
486
+ echo "IndraQ: using server Gradle $SYSTEM_VERSION for wrapper request $WRAPPER_VERSION (same major version)." >&2
487
+ fi
488
+ exec gradle "$@"
489
+ fi
490
+ fi
491
+
492
+ # Last resort: project wrapper. Its networkTimeout is raised to 120 seconds and
493
+ # it uses the shared Jenkins Gradle cache, so any download happens at most once.
494
+ exec bash ./gradlew "$@"
495
+ GRADLE_RUNNER
496
+ chmod +x .indraq-gradle
497
+
498
+ CACHED_GRADLE=''
499
+ if [ -n "$WRAPPER_VERSION" ] && [ -d "$GRADLE_USER_HOME/wrapper/dists" ]; then
500
+ CACHED_GRADLE=$(find "$GRADLE_USER_HOME/wrapper/dists" -type f -path "*/gradle-$WRAPPER_VERSION/bin/gradle" -print -quit 2>/dev/null || true)
501
+ fi
502
+
503
+ echo "========== Android Build Toolchain =========="
504
+ echo "Android SDK : $ANDROID_HOME"
505
+ echo "Shared Gradle cache: $GRADLE_USER_HOME"
506
+ echo "Wrapper shebang : $(head -n 1 gradlew)"
507
+ echo "Wrapper Gradle : ${WRAPPER_VERSION:-unknown}"
508
+ if [ -n "$SYSTEM_GRADLE_VERSION" ]; then
509
+ echo "System Gradle : $SYSTEM_GRADLE_VERSION"
510
+ else
511
+ echo "System Gradle : not in PATH"
512
+ fi
513
+ if [ -n "$CACHED_GRADLE" ]; then
514
+ echo "Cached wrapper : yes ($CACHED_GRADLE)"
515
+ elif [ -n "$SYSTEM_GRADLE_VERSION" ] && [ "$SYSTEM_GRADLE_VERSION" = "$WRAPPER_VERSION" ]; then
516
+ echo "Gradle source : matching server Gradle executable"
517
+ else
518
+ echo "Cached wrapper : not found; wrapper may download once (timeout raised to 120s)."
519
+ fi
520
+ if [ -d "$ANDROID_HOME/ndk" ]; then
521
+ echo "Installed NDKs:"
522
+ find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d -printf ' %f\n' | sort || true
523
+ fi
524
+ echo "============================================="
525
+ '''
526
+ }
527
+ }
528
+ }
529
+
530
+ stage('Apply Android Version') {
531
+ steps {
532
+ dir("${env.SOURCE_DIR}") {
533
+ sh '''#!/usr/bin/env bash
534
+ set -Eeuo pipefail
535
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
536
+
537
+ node <<'NODE'
538
+ const fs = require('fs');
539
+ const path = require('path');
540
+ const version = process.env.APP_VERSION;
541
+ const versionCode = process.env.APP_VERSION_CODE;
542
+ const candidates = [
543
+ path.join('android', 'app', 'build.gradle'),
544
+ path.join('android', 'app', 'build.gradle.kts')
545
+ ];
546
+ const gradleFile = candidates.find((candidate) => fs.existsSync(candidate));
547
+ if (!gradleFile) throw new Error('android/app/build.gradle or build.gradle.kts was not found.');
548
+
549
+ const original = fs.readFileSync(gradleFile, 'utf8');
550
+ let codeUpdated = false;
551
+ let nameUpdated = false;
552
+ const lines = original.split(/\\r?\\n/).map((line) => {
553
+ if (!codeUpdated) {
554
+ let match = line.match(/^(\\s*versionCode\\s*=\\s*).+$/);
555
+ if (match) {
556
+ codeUpdated = true;
557
+ return `${match[1]}${versionCode}`;
558
+ }
559
+ match = line.match(/^(\\s*versionCode\\s+).+$/);
560
+ if (match) {
561
+ codeUpdated = true;
562
+ return `${match[1]}${versionCode}`;
563
+ }
564
+ }
565
+ if (!nameUpdated) {
566
+ let match = line.match(/^(\\s*versionName\\s*=\\s*).+$/);
567
+ if (match) {
568
+ nameUpdated = true;
569
+ return `${match[1]}"${version}"`;
570
+ }
571
+ match = line.match(/^(\\s*versionName\\s+).+$/);
572
+ if (match) {
573
+ nameUpdated = true;
574
+ return `${match[1]}"${version}"`;
575
+ }
576
+ }
577
+ return line;
578
+ });
579
+
580
+ if (!codeUpdated || !nameUpdated) {
581
+ throw new Error(`Unable to apply Android version. versionCode updated=${codeUpdated}, versionName updated=${nameUpdated}. Check ${gradleFile}.`);
582
+ }
583
+ fs.writeFileSync(gradleFile, lines.join('\\n'));
584
+ console.log(`Android version applied in ${gradleFile}: versionName=${version}, versionCode=${versionCode}`);
585
+ NODE
586
+ '''
587
+ }
588
+ }
589
+ }
590
+
591
+ stage('Gradle Clean') {
592
+ when {
593
+ expression { params.BUILD_PROFILE == 'CLEAN' || params.BUILD_PROFILE == 'FULL_RESET' }
594
+ }
595
+ steps {
596
+ dir("${env.SOURCE_DIR}/android") {
597
+ sh '''#!/usr/bin/env bash
598
+ set -Eeuo pipefail
599
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
600
+ bash ./.indraq-gradle --stop || true
601
+ NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle clean --console=plain --no-build-cache
602
+ '''
603
+ }
604
+ }
605
+ }
606
+
607
+ stage('Build Android') {
608
+ steps {
609
+ dir("${env.SOURCE_DIR}/android") {
610
+ script {
611
+ if (params.BUILD_PROFILE == 'FAST') {
612
+ if (params.VERBOSE_BUILD) {
613
+ sh '''#!/usr/bin/env bash
614
+ set -Eeuo pipefail
615
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
616
+ NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --stacktrace --info
617
+ '''
618
+ } else {
619
+ sh '''#!/usr/bin/env bash
620
+ set -Eeuo pipefail
621
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
622
+ NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain
623
+ '''
624
+ }
625
+ } else if (params.VERBOSE_BUILD) {
626
+ sh '''#!/usr/bin/env bash
627
+ set -Eeuo pipefail
628
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
629
+ NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --no-build-cache --rerun-tasks --stacktrace --info
630
+ '''
631
+ } else {
632
+ sh '''#!/usr/bin/env bash
633
+ set -Eeuo pipefail
634
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
635
+ NODE_ENV="$NODE_ENV_VALUE" bash ./.indraq-gradle "$GRADLE_TASK" --console=plain --no-build-cache --rerun-tasks
636
+ '''
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+
643
+ stage('Generate Build Metadata') {
644
+ steps {
645
+ sh '''#!/usr/bin/env bash
646
+ set -Eeuo pipefail
647
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
648
+ BUILD_TIME=$(TZ=Asia/Kolkata date '+%Y-%m-%dT%H:%M:%S%z')
649
+ cat > "$BUILD_JSON_FILE" <<JSON
650
+ {
651
+ "app": "$APP_NAME",
652
+ "projectId": "$PROJECT_ID",
653
+ "version": "$APP_VERSION",
654
+ "versionCode": $APP_VERSION_CODE,
655
+ "projectType": "$PROJECT_TYPE",
656
+ "environment": "$BUILD_ENVIRONMENT",
657
+ "output": "$BUILD_OUTPUT",
658
+ "profile": "$BUILD_PROFILE",
659
+ "buildNumber": "$BUILD_NUMBER",
660
+ "sourceRevision": "$SOURCE_REVISION",
661
+ "sourceDirty": "$SOURCE_DIRTY",
662
+ "sourceSha256": "$SOURCE_SHA256",
663
+ "buildTime": "$BUILD_TIME"
664
+ }
665
+ JSON
666
+ cat "$BUILD_JSON_FILE"
667
+ '''
668
+ }
669
+ }
670
+
671
+ stage('Collect Artifacts') {
672
+ steps {
673
+ sh '''#!/usr/bin/env bash
674
+ set -Eeuo pipefail
675
+ exec > >(tee -a "$BUILD_LOG_FILE") 2>&1
676
+
677
+ rm -rf "$WORKSPACE/jenkins-artifacts"
678
+ mkdir -p "$WORKSPACE/jenkins-artifacts"
679
+
680
+ [ -d "$ARTIFACT_SEARCH_DIR" ] || { echo "Artifact directory does not exist: $ARTIFACT_SEARCH_DIR"; exit 1; }
681
+ mapfile -t ARTIFACTS < <(find "$ARTIFACT_SEARCH_DIR" -maxdepth 1 -type f -name "$ARTIFACT_FILE_PATTERN" | sort)
682
+ [ "${#ARTIFACTS[@]}" -gt 0 ] || { echo "No .$ARTIFACT_EXT artifact found."; exit 1; }
683
+
684
+ PRIMARY_ARTIFACT="${ARTIFACTS[0]}"
685
+ SAFE_APP_NAME=$(printf '%s' "$APP_NAME" | sed -E 's/[^A-Za-z0-9._-]+/_/g; s/^_+//; s/_+$//')
686
+ [ -n "$SAFE_APP_NAME" ] || SAFE_APP_NAME="app"
687
+ BUILD_STAMP=$(TZ=Asia/Kolkata date '+%Y-%m-%d_%H%M')
688
+ PRIMARY_ARTIFACT_NAME="${SAFE_APP_NAME}_${APP_VERSION}_${APP_VERSION_CODE}_${BUILD_STAMP}.${ARTIFACT_EXT}"
689
+
690
+ cp "$PRIMARY_ARTIFACT" "$WORKSPACE/jenkins-artifacts/$PRIMARY_ARTIFACT_NAME"
691
+ cp "$PRIMARY_ARTIFACT" "$BUILD_ARTIFACT_DIR/$PRIMARY_ARTIFACT_NAME"
692
+ cp "$BUILD_LOG_FILE" "$WORKSPACE/jenkins-artifacts/build.log"
693
+ cp "$BUILD_JSON_FILE" "$WORKSPACE/jenkins-artifacts/build.json"
694
+ echo "$PRIMARY_ARTIFACT_NAME" > "$WORKSPACE/jenkins-artifacts/primary-artifact-name.txt"
695
+
696
+ echo "Primary artifact: $PRIMARY_ARTIFACT_NAME"
697
+ echo "Artifact timestamp timezone: Asia/Kolkata (IST)"
698
+ '''
699
+ }
700
+ }
701
+
702
+ stage('Archive & Publish Download Link') {
703
+ steps {
704
+ script {
705
+ archiveArtifacts(artifacts: 'jenkins-artifacts/**/*', allowEmptyArchive: false, fingerprint: true)
706
+ env.PRIMARY_ARTIFACT_NAME = readFile("${env.WORKSPACE}/jenkins-artifacts/primary-artifact-name.txt").trim()
707
+ env.PRIMARY_ARTIFACT_URL = "${env.BUILD_URL}artifact/jenkins-artifacts/${env.PRIMARY_ARTIFACT_NAME}"
708
+ currentBuild.description = "Download: ${env.PRIMARY_ARTIFACT_NAME}"
709
+ echo '=============================================='
710
+ echo 'DOWNLOAD READY'
711
+ echo "File: ${env.PRIMARY_ARTIFACT_NAME}"
712
+ echo "URL: ${env.PRIMARY_ARTIFACT_URL}"
713
+ echo '=============================================='
714
+ }
715
+ }
716
+ }
717
+ }
718
+
719
+ post {
720
+ always {
721
+ sh '''#!/usr/bin/env bash
722
+ set +e
723
+ # Never leave the developer's environment file in the Jenkins workspace.
724
+ if [ -n "${SOURCE_DIR:-}" ] && [ "$SOURCE_DIR" != "/" ]; then
725
+ rm -f "$SOURCE_DIR/.env" "$SOURCE_DIR/ENV_FILE"
726
+ fi
727
+ if [ -n "${WORKSPACE:-}" ] && [ "$WORKSPACE" != "/" ]; then
728
+ rm -f "$WORKSPACE/ENV_FILE" "$WORKSPACE/SOURCE_BUNDLE"
729
+ fi
730
+ '''
731
+ }
732
+ success {
733
+ echo '=============================================='
734
+ echo 'BUILD SUCCESSFUL'
735
+ echo "App: ${params.APP_NAME}"
736
+ echo "Version: ${params.APP_VERSION}"
737
+ echo "versionCode: ${params.APP_VERSION_CODE}"
738
+ echo "Environment: ${params.BUILD_ENVIRONMENT}"
739
+ echo "Output: ${params.BUILD_OUTPUT}"
740
+ echo "Profile: ${params.BUILD_PROFILE}"
741
+ echo "File: ${env.PRIMARY_ARTIFACT_NAME}"
742
+ echo "Download: ${env.PRIMARY_ARTIFACT_URL}"
743
+ echo "Server backup: ${env.BUILD_ARTIFACT_DIR}"
744
+ echo '=============================================='
745
+ }
746
+ failure {
747
+ echo '=============================================='
748
+ echo 'BUILD FAILED'
749
+ echo "App: ${params.APP_NAME}"
750
+ echo "Version: ${params.APP_VERSION}"
751
+ echo "versionCode: ${params.APP_VERSION_CODE}"
752
+ echo "Environment: ${params.BUILD_ENVIRONMENT}"
753
+ echo "Output: ${params.BUILD_OUTPUT}"
754
+ echo "Profile: ${params.BUILD_PROFILE}"
755
+ echo 'Check console logs above.'
756
+ echo '=============================================='
757
+ }
758
+ }
759
+ }