-
Notifications
You must be signed in to change notification settings - Fork 393
286 lines (275 loc) · 12.5 KB
/
reusable-lib-workflow.yaml
File metadata and controls
286 lines (275 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
on:
workflow_call:
inputs:
lib:
required: true
type: string
is_pr:
type: boolean
default: false
jobs:
test-android:
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/.github/DangerFiles/Gemfile
steps:
- uses: actions/checkout@v4
if: ${{ inputs.is_pr }}
with:
# We need a sufficient depth or Danger will occasionally run into issues checking which files were modified.
fetch-depth: 100
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/checkout@v4
if: ${{ ! inputs.is_pr }}
with:
ref: ${{ github.head_ref }}
- name: Install Dependencies
env:
TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }}
# On PR runs, only SalesforceReact consumes the bundled index.android.bundle,
# so skip the yarn install + react-native bundle step for every other lib to
# save ~3-5 min per matrix job. Nightly runs still produce the bundle.
SKIP_REACT_NATIVE_BUNDLE: ${{ (inputs.is_pr && inputs.lib != 'SalesforceReact') && '1' || '0' }}
run: |
./install.sh
echo $TEST_CREDENTIALS > ./shared/test/test_credentials.json
- name: Hybrid Dependencies
if: ${{ inputs.lib == 'SalesforceHybrid' }}
run: |
npm install -g cordova
- name: React Native Dependencies
if: ${{ inputs.lib == 'SalesforceReact' }}
run: npm install -g typescript
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '21'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- uses: gradle/actions/setup-gradle@v4
with:
# This is the actual Gradle version (not AGP), which can be found in the gradle-wrapper.properties file.
gradle-version: "8.14.3"
add-job-summary: on-failure
add-job-summary-as-pr-comment: on-failure
- name: Run Lint
if: ${{ inputs.is_pr }}
run: ./gradlew libs:${{ inputs.lib }}:lint
- uses: ruby/setup-ruby@v1
if: ${{ inputs.is_pr }}
with:
ruby-version: '3.2'
bundler-cache: true
- name: Report Static Analysis
if: ${{ inputs.is_pr }}
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LIB: ${{ inputs.lib }}
run: bundle exec danger --dangerfile=.github/DangerFiles/StaticAnalysis.rb --danger_id="${{ inputs.lib }}"
- name: Build for Testing
if: success() || failure()
run: |
./gradlew libs:${{ inputs.lib }}:assembleAndroidTest
./gradlew native:NativeSampleApps:RestExplorer:assembleDebug
- name: Run Unit Tests
if: success() || failure()
continue-on-error: true
run: ./gradlew libs:${{ inputs.lib }}:testDebugUnitTest --continue
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: success() || failure()
with:
check_name: ${{ inputs.lib }} Unit Test Results
files: |
libs/${{ inputs.lib }}/build/test-results/testDebugUnitTest/*.xml
comment_mode: off
fail_on: nothing
- name: Archive Unit Test Results
uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: unit-test-results-${{ inputs.lib }}
path: libs/${{ inputs.lib }}/build/test-results/testDebugUnitTest/*.xml
- name: Generate Unit Test Coverage Report
if: success() || failure()
run: ./gradlew libs:${{ inputs.lib }}:unitTestCoverageReport
- uses: codecov/codecov-action@v5
if: success() || failure()
with:
files: libs/${{ inputs.lib }}/build/reports/jacoco/unitTestCoverageReport/unitTestCoverageReport.xml
flags: ${{ inputs.lib }}-unit-tests
name: ${{ inputs.lib }}-unit-test-coverage
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Archive Unit Test Coverage Report
uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: unit-test-coverage-${{ inputs.lib }}
path: libs/${{ inputs.lib }}/build/reports/jacoco/unitTestCoverageReport/
- uses: 'google-github-actions/auth@v2'
if: success() || failure()
with:
credentials_json: '${{ secrets.GCLOUD_SERVICE_KEY }}'
- uses: 'google-github-actions/setup-gcloud@v2'
if: success() || failure()
- name: Validate Shard Config
if: success() || failure()
run: |
SHARD_CONFIG=".github/test-shards/${{ inputs.lib }}.json"
if [ -f "$SHARD_CONFIG" ]; then
# Extract sorted class names from non-remaining shards (strip "class " prefix)
CLASSES=$(jq -r '[.shards[] | select(.name != "remaining") | .targets[] | select(startswith("class ")) | ltrimstr("class ")] | sort | .[]' "$SHARD_CONFIG")
# Extract sorted notClass names from remaining shard (strip "notClass " prefix)
NOT_CLASSES=$(jq -r '[.shards[] | select(.name == "remaining") | .targets[] | select(startswith("notClass ")) | ltrimstr("notClass ")] | sort | .[]' "$SHARD_CONFIG")
if [ "$CLASSES" != "$NOT_CLASSES" ]; then
echo "::error::Shard config mismatch in $SHARD_CONFIG - classes in shards must exactly match notClass entries in remaining shard"
echo "Difference:"
diff <(echo "$CLASSES") <(echo "$NOT_CLASSES") || true
exit 1
fi
echo "✓ Shard config valid for ${{ inputs.lib }} ($(echo "$CLASSES" | wc -l | tr -d ' ') classes)"
else
echo "No shard config for ${{ inputs.lib }}, skipping validation"
fi
- name: Run Tests
continue-on-error: true
if: success() || failure()
env:
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
PR_API_VERSION: "35"
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
IS_PR: ${{ inputs.is_pr }}
run: |
LEVELS_TO_TEST=$FULL_API_RANGE
RETRIES=0
if $IS_PR ; then
LEVELS_TO_TEST=$PR_API_VERSION
fi
# Build test-targets-for-shard arguments from config file
SHARD_CONFIG=".github/test-shards/${{ inputs.lib }}.json"
SHARD_ARGS=()
if [ -f "$SHARD_CONFIG" ]; then
NUM_SHARDS=$(jq '.shards | length' "$SHARD_CONFIG")
for i in $(seq 0 $((NUM_SHARDS - 1))); do
# Join targets with semicolons for this shard
TARGETS=$(jq -r ".shards[$i].targets | join(\";\")" "$SHARD_CONFIG")
SHARD_ARGS+=("--test-targets-for-shard=\"${TARGETS}\"")
done
else
SHARD_ARGS=("")
fi
mkdir firebase_results
gcloud components install beta --quiet
for LEVEL in $LEVELS_TO_TEST
do
GCLOUD_RESULTS_DIR=${{ inputs.lib }}-api-${LEVEL}-build-${{github.run_number}}
eval gcloud beta firebase test android run \
--project mobile-apps-firebase-test \
--type instrumentation \
--app "native/NativeSampleApps/RestExplorer/build/outputs/apk/debug/RestExplorer-debug.apk" \
--test=libs/${{ inputs.lib }}/build/outputs/apk/androidTest/debug/${{ inputs.lib }}-debug-androidTest.apk \
--device model=MediumPhone.arm,version=${LEVEL},locale=en,orientation=portrait \
--environment-variables coverage=true,coverageFile="/sdcard/coverage.ec" \
--directories-to-pull=/sdcard \
--results-dir=${GCLOUD_RESULTS_DIR} \
--results-history-name=${{ inputs.lib }} \
--timeout=30m --no-auto-google-login --no-record-video --no-performance-metrics \
"${SHARD_ARGS[@]}" \
--num-flaky-test-attempts=${RETRIES} || true
done
- name: Copy Test Results
continue-on-error: true
if: success() || failure()
env:
# Most used according to https://gs.statcounter.com/android-version-market-share/mobile-tablet/worldwide
PR_API_VERSION: "35"
FULL_API_RANGE: "28 29 30 31 32 33 34 35 36"
IS_PR: ${{ inputs.is_pr }}
run: |
LEVELS_TO_TEST=$FULL_API_RANGE
if $IS_PR ; then
LEVELS_TO_TEST=$PR_API_VERSION
fi
for LEVEL in $LEVELS_TO_TEST
do
GCLOUD_RESULTS_DIR=${{ inputs.lib }}-api-${LEVEL}-build-${{github.run_number}}
BUCKET_PATH="gs://test-lab-w87i9sz6q175u-kwp8ium6js0zw/${GCLOUD_RESULTS_DIR}"
gsutil ls ${BUCKET_PATH} > /dev/null 2>&1
if [ $? == 0 ] ; then
# Copy XML file for test reporting
if gsutil ls "${BUCKET_PATH}/*test_results_merged.xml" > /dev/null 2>&1; then
# Sharded runs produce test_results_merged.xml at top level
gsutil cp "${BUCKET_PATH}/*test_results_merged.xml" firebase_results/api_${LEVEL}_test_result.xml
else
# Pass 1: copy original (non-rerun) results
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep -v "rerun"); do
gsutil cp "${RESULT_FILE}" "firebase_results/api_${LEVEL}_test_result.xml"
done
# Pass 2: merge rerun testcases into originals so check_retries detects flaky tests
for RESULT_FILE in $(gsutil ls "${BUCKET_PATH}/*/test_result_1.xml" 2>/dev/null | grep "rerun"); do
RERUN_TMP="firebase_results/api_${LEVEL}_rerun_tmp.xml"
ORIG_FILE="firebase_results/api_${LEVEL}_test_result.xml"
gsutil cp "${RESULT_FILE}" "${RERUN_TMP}"
python3 - "${ORIG_FILE}" "${RERUN_TMP}" "${ORIG_FILE}" << 'PYEOF'
import sys, xml.etree.ElementTree as ET
orig = ET.parse(sys.argv[1])
rerun = ET.parse(sys.argv[2])
def suite(t):
r = t.getroot()
return r if r.tag == 'testsuite' else r.find('testsuite')
os_el, rs_el = suite(orig), suite(rerun)
failed_keys = set()
for tc in os_el.findall('testcase'):
if tc.find('failure') is not None or tc.find('error') is not None:
failed_keys.add(f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}")
added = 0
for tc in rs_el.findall('testcase'):
if f"{tc.get('name','')}|{tc.get('classname','')}|{tc.get('file','')}" in failed_keys:
os_el.append(tc)
added += 1
os_el.set('tests', str(int(os_el.get('tests','0')) + added))
with open(sys.argv[3], 'w') as f:
f.write(ET.tostring(orig.getroot(), encoding='unicode'))
PYEOF
rm "${RERUN_TMP}"
done
fi
# Copy all shard data for code coverage (only needed for one level)
if [ "$LEVEL" == "$PR_API_VERSION" ] ; then
mkdir -p firebase
gsutil -m cp -r -U "${BUCKET_PATH}/*" ./firebase/
fi
fi
done
- name: Test Report
uses: mikepenz/action-junit-report@v6
if: success() || failure()
with:
check_name: ${{ inputs.lib }} Test Results
job_name: ${{ inputs.lib }} Test Results
require_tests: true
check_retries: true
flaky_summary: true
fail_on_failure: true
group_reports: false
include_passed: true
include_empty_in_summary: false
simplified_summary: true
report_paths: 'firebase_results/**.xml'
- name: Archive Test Results
uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: test-results-${{ inputs.lib }}
path: 'firebase_results/**.xml'
- name: Convert Code Coverage
if: success() || failure()
run: ./gradlew libs:${{ inputs.lib }}:convertCodeCoverage
- uses: codecov/codecov-action@v5
if: success() || failure()
with:
files: libs/${{ inputs.lib }}/build/reports/jacoco/convertedCodeCoverage/convertedCodeCoverage.xml
flags: ${{ inputs.lib }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}