indraq_cli 2.1.0 → 2.2.0
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.
- package/CHANGELOG_v2.1.0.md +27 -27
- package/CHANGELOG_v2.2.0.md +52 -0
- package/LICENSE +21 -21
- package/README.md +27 -14
- package/RELEASE_CHECKS_v2.2.0.md +27 -0
- package/UPGRADE_v2.0_to_v2.1.md +68 -68
- package/UPGRADE_v2.1_to_v2.2.md +62 -0
- package/cloud-api/.dockerignore +18 -18
- package/cloud-api/.env.example +14 -14
- package/cloud-api/Dockerfile +19 -19
- package/cloud-api/README.md +93 -93
- package/cloud-api/dist/index.js +265 -27
- package/cloud-api/node_modules/.package-lock.json +1 -1
- package/cloud-api/package-lock.json +2 -2
- package/cloud-api/package.json +1 -1
- package/cloud-api/sql/001_init.sql +8 -8
- package/cloud-api/sql/002_user_providers_and_project_resources.sql +40 -40
- package/cloud-api/sql/003_environment_infrastructure.sql +19 -19
- package/cloud-api/sql/004_managed_aws_credentials.sql +20 -20
- package/cloud-api/sql/005_project_members_and_resource_grants.sql +22 -22
- package/cloud-api/sql/006_catalog_identity_password_security.sql +96 -96
- package/cloud-api/sql/007_aws_access_key_rotation.sql +16 -16
- package/cloud-api/sql/008_provider_scopes.sql +91 -91
- package/cloud-api/sql/009_scope_user_secrets_and_project_lifecycle.sql +13 -0
- package/cloud-api/src/index.ts +164 -27
- package/cloud-api/tsconfig.json +18 -18
- package/dist/cli/help-content.d.ts.map +1 -1
- package/dist/cli/help-content.js +11 -5
- package/dist/cli/help-content.js.map +1 -1
- package/dist/modules/access/commands/access.command.js +30 -30
- package/dist/modules/aws/services/route53-lookup.service.js.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.d.ts +4 -0
- package/dist/modules/cloud/commands/cloud.command.d.ts.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.js +52 -1
- package/dist/modules/cloud/commands/cloud.command.js.map +1 -1
- package/dist/modules/cloud/index.d.ts.map +1 -1
- package/dist/modules/cloud/index.js +4 -1
- package/dist/modules/cloud/index.js.map +1 -1
- package/dist/modules/cloud/services/cloud-jenkins.service.js +71 -71
- package/dist/modules/cloud/services/user-provider.service.d.ts +2 -8
- package/dist/modules/cloud/services/user-provider.service.d.ts.map +1 -1
- package/dist/modules/cloud/services/user-provider.service.js +7 -35
- package/dist/modules/cloud/services/user-provider.service.js.map +1 -1
- package/dist/modules/deploy/commands/build.command.d.ts +1 -0
- package/dist/modules/deploy/commands/build.command.d.ts.map +1 -1
- package/dist/modules/deploy/commands/build.command.js +12 -5
- package/dist/modules/deploy/commands/build.command.js.map +1 -1
- package/dist/modules/deploy/index.d.ts.map +1 -1
- package/dist/modules/deploy/index.js +11 -4
- package/dist/modules/deploy/index.js.map +1 -1
- package/dist/modules/prebuild/commands/prebuild.command.d.ts +0 -4
- package/dist/modules/prebuild/commands/prebuild.command.d.ts.map +1 -1
- package/dist/modules/prebuild/commands/prebuild.command.js +34 -38
- package/dist/modules/prebuild/commands/prebuild.command.js.map +1 -1
- package/dist/modules/prebuild/index.d.ts.map +1 -1
- package/dist/modules/prebuild/index.js +4 -8
- package/dist/modules/prebuild/index.js.map +1 -1
- package/dist/modules/scaffold/services/docker-template.service.js +95 -95
- package/dist/modules/scopes/commands/scope.command.d.ts.map +1 -1
- package/dist/modules/scopes/commands/scope.command.js +3 -47
- package/dist/modules/scopes/commands/scope.command.js.map +1 -1
- package/dist/modules/scopes/services/scope-native.service.js +2 -2
- package/dist/modules/scopes/services/scope-native.service.js.map +1 -1
- package/dist/modules/users/commands/user.command.d.ts +1 -0
- package/dist/modules/users/commands/user.command.d.ts.map +1 -1
- package/dist/modules/users/commands/user.command.js +15 -1
- package/dist/modules/users/commands/user.command.js.map +1 -1
- package/dist/modules/users/index.d.ts.map +1 -1
- package/dist/modules/users/index.js +1 -0
- package/dist/modules/users/index.js.map +1 -1
- package/dist/modules/users/providers/jenkins-user.provider.js +95 -95
- package/dist/shared/config/config-files.js +7 -7
- package/dist/shared/jenkins/jenkins.service.d.ts.map +1 -1
- package/dist/shared/jenkins/jenkins.service.js +13 -8
- package/dist/shared/jenkins/jenkins.service.js.map +1 -1
- package/dist/shared/jenkins/role-strategy-groovy.js +84 -84
- package/docs/ARCHITECTURE.md +29 -29
- package/docs/CREATE-DEPLOYMENT-reference.groovy +1105 -1105
- package/package.json +9 -5
- package/templates/jenkins/CREATE-DEPLOYMENT.groovy +1105 -1105
- package/templates/jenkins/Jenkinsfile-Mobile-App +759 -759
- 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
|
+
}
|