11name : QA Android UI Tests
22
3+ # Concurrency lock:
4+ # - If androidDeviceId is set, we lock per-device so only one run can use that specific phone at a time (other devices can run in parallel).
5+ # - If androidDeviceId is empty ("auto"), we lock the shared device pool so only one auto-run uses the farm at a time (other auto-runs queue).
6+ concurrency :
7+ group : qa-android-ui-tests-office-${{ inputs.androidDeviceId || 'auto' }}
8+ cancel-in-progress : false
9+
310on :
411 workflow_dispatch :
512 inputs :
4451 - production
4552 default : internal release candidate
4653
47- buildType :
48- description : " Build type"
49- required : true
50- type : choice
51- options :
52- - release
53- - debug
54- - compat
55- default : release
56-
5754 TAGS :
5855 description : " Tags: '@regression' OR '@TC-8143'."
5956 required : false
6057 default : " "
6158 type : string
6259
63- branch :
64- description : " Branch"
65- required : true
66- default : " develop"
67- type : string
68-
69- backendType :
70- description : " Backend."
71- required : true
72- default : " staging"
73- type : string
74-
7560 testinyRunName :
7661 description : " TESTINY_RUN_NAME."
7762 required : false
@@ -84,145 +69,216 @@ on:
8469 default : " "
8570 type : string
8671
87- callingServiceEnv :
88- description : " Calling service environment."
89- required : true
90- type : choice
91- options :
92- - dev
93- - custom
94- - master
95- - avs
96- - qa
97- - edge
98- - staging
99- - prod
100- default : dev
101-
102- callingServiceUrl :
103- description : " Calling service URL."
104- required : true
105- default : " loadbalanced"
106- type : string
107-
108- deflakeCount :
109- description : " Rerun only failed tests."
110- required : true
111- default : 1
112- type : number
72+ permissions :
73+ contents : read
11374
11475jobs :
76+ # Validate user inputs and derive selectors.
11577 validate-and-resolve-inputs :
116- name : Validate + resolve selectors (no execution)
78+ name : Validate + resolve selectors
11779 runs-on : ubuntu-latest
11880
11981 outputs :
120- resolvedTestCaseId : ${{ steps.resolve.outputs.testCaseId }}
121- resolvedCategory : ${{ steps.resolve.outputs.category }}
122- resolvedTagKey : ${{ steps.resolve.outputs.tagKey }}
123- resolvedTagValue : ${{ steps.resolve.outputs.tagValue }}
82+ resolvedTestCaseId : ${{ steps.resolve_selector.outputs.testCaseId }}
83+ resolvedCategory : ${{ steps.resolve_selector.outputs.category }}
12484
12585 steps :
86+ - name : Checkout repository
87+ uses : actions/checkout@v4
88+
89+ # Validate upgrade inputs before runner work starts.
12690 - name : Validate upgrade inputs
12791 shell : bash
128- run : |
129- set -euo pipefail
130- if [[ "${{ inputs.isUpgrade }}" == "true" && -z "${{ inputs.oldBuildNumber }}" ]]; then
131- echo "ERROR: oldBuildNumber is REQUIRED when isUpgrade=true"
132- exit 1
133- fi
92+ env :
93+ IS_UPGRADE : ${{ inputs.isUpgrade }}
94+ OLD_BUILD_NUMBER : ${{ inputs.oldBuildNumber }}
95+ run : bash scripts/qa_android_ui_tests/validation.sh validate-upgrade-inputs
13496
97+ # Resolve TAGS into CI selectors and expose them as job outputs.
13598 - name : Resolve selector from TAGS
136- id : resolve
99+ id : resolve_selector
137100 shell : bash
138- run : |
139- set -euo pipefail
140-
141- # We only use TAGS in this UI (no extra selector inputs)
142- TESTCASE_ID=""
143- CATEGORY=""
144- TAG_KEY=""
145- TAG_VALUE=""
146-
147- TAGS_RAW="${{ inputs.TAGS }}"
148-
149- trim() { echo "$1" | xargs; }
150-
151- # If TAGS is provided, derive selector
152- if [[ -n "$(trim "${TAGS_RAW}")" ]]; then
153- # TAGS can be comma-separated; use first non-empty token
154- sel=""
155- IFS=',' read -ra parts <<< "${TAGS_RAW}"
156- for p in "${parts[@]}"; do
157- t="$(trim "$p")"
158- if [[ -n "$t" ]]; then
159- sel="$t"
160- break
161- fi
162- done
163-
164- # Strip leading @ if present
165- sel="${sel#@}"
166- sel="$(trim "$sel")"
167-
168- # @TC-8143 -> testCaseId
169- if [[ "$sel" =~ ^TC-[0-9]+$ ]]; then
170- TESTCASE_ID="$sel"
171-
172- # @key:value -> tagKey/tagValue (kept for future parity)
173- elif [[ "$sel" == *:* ]]; then
174- k="$(trim "${sel%%:*}")"
175- v="$(trim "${sel#*:}")"
176- if [[ -z "$k" || -z "$v" ]]; then
177- echo "ERROR: Invalid TAGS format '${TAGS_RAW}'. Expected '@key:value' e.g. @criticalFlow:groupCallChat"
178- exit 1
179- fi
180- TAG_KEY="$k"
181- TAG_VALUE="$v"
182-
183- # @regression -> category
184- else
185- CATEGORY="$sel"
186- fi
187- fi
188-
189- # Safety: prevent partial key/value
190- if [[ -n "$TAG_KEY" && -z "$TAG_VALUE" ]]; then
191- echo "ERROR: tagKey provided but tagValue is empty."
192- exit 1
193- fi
194- if [[ -z "$TAG_KEY" && -n "$TAG_VALUE" ]]; then
195- echo "ERROR: tagValue provided but tagKey is empty."
196- exit 1
197- fi
198-
199- echo "testCaseId=$TESTCASE_ID" >> "$GITHUB_OUTPUT"
200- echo "category=$CATEGORY" >> "$GITHUB_OUTPUT"
201- echo "tagKey=$TAG_KEY" >> "$GITHUB_OUTPUT"
202- echo "tagValue=$TAG_VALUE" >> "$GITHUB_OUTPUT"
203-
204- - name : Print final resolved inputs (for reviewers)
101+ env :
102+ TAGS_RAW : ${{ inputs.TAGS }}
103+ run : bash scripts/qa_android_ui_tests/validation.sh resolve-selector-from-tags
104+
105+ # Print resolved values for traceability in workflow logs.
106+ - name : Print resolved values
205107 shell : bash
206- run : |
207- set -euo pipefail
208- echo "=== RAW INPUTS ==="
209- echo "appBuildNumber=${{ inputs.appBuildNumber }}"
210- echo "isUpgrade=${{ inputs.isUpgrade }}"
211- echo "oldBuildNumber=${{ inputs.oldBuildNumber }}"
212- echo "enforceAppInstall=${{ inputs.enforceAppInstall }}"
213- echo "flavor=${{ inputs.flavor }}"
214- echo "buildType=${{ inputs.buildType }}"
215- echo "TAGS=${{ inputs.TAGS }}"
216- echo "branch=${{ inputs.branch }}"
217- echo "backendType=${{ inputs.backendType }}"
218- echo "testinyRunName=${{ inputs.testinyRunName }}"
219- echo "androidDeviceId=${{ inputs.androidDeviceId }}"
220- echo "callingServiceEnv=${{ inputs.callingServiceEnv }}"
221- echo "callingServiceUrl=${{ inputs.callingServiceUrl }}"
222- echo "deflakeCount=${{ inputs.deflakeCount }}"
223- echo ""
224- echo "=== RESOLVED SELECTORS ==="
225- echo "testCaseId=${{ steps.resolve.outputs.testCaseId }}"
226- echo "category=${{ steps.resolve.outputs.category }}"
227- echo "tagKey=${{ steps.resolve.outputs.tagKey }}"
228- echo "tagValue=${{ steps.resolve.outputs.tagValue }}"
108+ env :
109+ FLAVOR_INPUT : ${{ inputs.flavor }}
110+ RESOLVED_TESTCASE_ID : ${{ steps.resolve_selector.outputs.testCaseId }}
111+ RESOLVED_CATEGORY : ${{ steps.resolve_selector.outputs.category }}
112+ run : bash scripts/qa_android_ui_tests/validation.sh print-resolved-values
113+
114+ run-android-ui-tests :
115+ name : Run Android UI tests
116+ runs-on :
117+ - self-hosted
118+ - Linux
119+ - X64
120+ - office
121+ - android-qa
122+
123+ needs : validate-and-resolve-inputs
124+ permissions :
125+ contents : write
126+
127+ env :
128+ AWS_REGION : eu-west-1
129+ S3_BUCKET : ${{ secrets.AWS_S3_BUCKET }}
130+ OP_VAULT : " Test Automation"
131+ FLAVORS_CONFIG_PATH : " /etc/android-qa/flavors.json"
132+
133+ defaults :
134+ run :
135+ shell : bash
136+
137+ steps :
138+ - name : Checkout (with submodules)
139+ uses : actions/checkout@v4
140+ with :
141+ clean : true
142+ submodules : recursive
143+
144+ - name : Set up Java 17
145+ uses : actions/setup-java@v4
146+ with :
147+ distribution : temurin
148+ java-version : " 17"
149+ cache : gradle
150+
151+ - name : Set up Android SDK (ANDROID_HOME + adb)
152+ uses : android-actions/setup-android@v3
153+
154+ # Verify required CLIs on the runner before setup continues.
155+ - name : Ensure required tools exist
156+ run : bash scripts/qa_android_ui_tests/execution_setup.sh ensure-required-tools
157+
158+ # Flavor resolution is runner-driven (from /etc/android-qa/flavors.json), not hardcoded in repo.
159+ # This bash subcommand exports S3_FOLDER, APP_ID, and PACKAGES_TO_UNINSTALL for downstream steps.
160+ - name : Resolve flavor (runner config)
161+ id : resolve_flavor
162+ env :
163+ FLAVOR_INPUT : ${{ inputs.flavor }}
164+ run : bash scripts/qa_android_ui_tests/execution_setup.sh resolve-flavor
165+
166+ - name : Configure AWS credentials (for S3)
167+ uses : aws-actions/configure-aws-credentials@v4
168+ with :
169+ aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }}
170+ aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }}
171+ aws-region : eu-west-1
172+
173+ # Download app APK(s) from S3 and export resolved build metadata.
174+ - name : Download APK(s) from S3
175+ id : download_apks
176+ env :
177+ APP_BUILD_NUMBER : ${{ inputs.appBuildNumber }}
178+ IS_UPGRADE : ${{ inputs.isUpgrade }}
179+ OLD_BUILD_NUMBER : ${{ inputs.oldBuildNumber }}
180+ run : bash scripts/qa_android_ui_tests/execution_setup.sh download-apks
181+
182+ # Select device(s): use input device when provided, otherwise auto-pick.
183+ - name : Detect target device(s)
184+ env :
185+ TARGET_DEVICE_ID : ${{ inputs.androidDeviceId }}
186+ RESOLVED_TESTCASE_ID : ${{ needs.validate-and-resolve-inputs.outputs.resolvedTestCaseId }}
187+ run : bash scripts/qa_android_ui_tests/execution_setup.sh detect-target-devices
188+
189+ # Install app/test prerequisites on each selected device.
190+ - name : Install APK(s) on device(s)
191+ env :
192+ ENFORCE_APP_INSTALL : ${{ inputs.enforceAppInstall }}
193+ IS_UPGRADE : ${{ inputs.isUpgrade }}
194+ run : bash scripts/qa_android_ui_tests/execution_setup.sh install-apks-on-devices
195+
196+ - name : Install 1Password CLI
197+ uses : 1password/install-cli-action@v2
198+
199+ # Fetch runtime secrets only for this run.
200+ - name : Fetch secrets.json (runtime only)
201+ env :
202+ OP_SERVICE_ACCOUNT_TOKEN : ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
203+ run : bash scripts/qa_android_ui_tests/execution_setup.sh fetch-runtime-secrets
204+
205+ # Build androidTest APK once and reuse it across selected devices.
206+ - name : Build test APK (assemble once)
207+ run : bash scripts/qa_android_ui_tests/execution_setup.sh build-test-apk
208+
209+ # Resolve the built androidTest APK path for execution steps.
210+ - name : Resolve test APK path
211+ run : bash scripts/qa_android_ui_tests/execution_setup.sh resolve-test-apk-path
212+
213+ # Resolve AndroidX Test Services APKs required for TestStorage/Allure.
214+ - name : Resolve AndroidX Test Services APKs (for Allure TestStorage)
215+ run : bash scripts/qa_android_ui_tests/execution_setup.sh resolve-test-services-apks
216+
217+ # Run instrumentation on selected devices and stream per-device logs.
218+ - name : Run UI tests (one shard per device, adb instrumentation)
219+ env :
220+ RESOLVED_TESTCASE_ID : ${{ needs.validate-and-resolve-inputs.outputs.resolvedTestCaseId }}
221+ RESOLVED_CATEGORY : ${{ needs.validate-and-resolve-inputs.outputs.resolvedCategory }}
222+ IS_UPGRADE : ${{ inputs.isUpgrade }}
223+ run : bash scripts/qa_android_ui_tests/run_ui_tests.sh
224+
225+ # Remove runtime secrets before report generation and publish steps.
226+ - name : Remove runtime secrets (before Allure/Pages)
227+ if : always()
228+ run : bash scripts/qa_android_ui_tests/reporting.sh remove-runtime-secrets
229+
230+ # Pull raw allure-results from each device even when tests fail.
231+ - name : Pull Allure results from device(s)
232+ if : always()
233+ env :
234+ OUT_DIR : ${{ runner.temp }}/allure-results
235+ run : bash scripts/qa_android_ui_tests/reporting.sh pull-allure-results
236+
237+ # Merge per-device results and attach run metadata labels.
238+ - name : Merge Allure results (add device label)
239+ if : always()
240+ env :
241+ OUT_DIR : ${{ runner.temp }}/allure-results
242+ MERGED_DIR : ${{ runner.temp }}/allure-results-merged
243+ REAL_BUILD_NUMBER : ${{ env.REAL_BUILD_NUMBER }}
244+ NEW_APK_NAME : ${{ env.NEW_APK_NAME }}
245+ INPUT_TAGS : ${{ inputs.TAGS }}
246+ run : bash scripts/qa_android_ui_tests/reporting.sh merge-allure-results
247+
248+ # Generate static Allure HTML from merged results.
249+ - name : Generate Allure HTML report
250+ if : always()
251+ env :
252+ MERGED_DIR : ${{ runner.temp }}/allure-results-merged
253+ REPORT_DIR : ${{ runner.temp }}/allure-report
254+ run : bash scripts/qa_android_ui_tests/reporting.sh generate-allure-report
255+
256+ - name : Checkout GitHub Pages branch
257+ if : always()
258+ uses : actions/checkout@v4
259+ with :
260+ ref : gh-pages
261+ path : gh-pages
262+ fetch-depth : 1
263+ persist-credentials : true
264+
265+ # Publish report snapshots and apply retention cleanup.
266+ - name : Publish Allure report to Pages branch
267+ if : always()
268+ env :
269+ REPORT_DIR : ${{ runner.temp }}/allure-report
270+ PAGES_DIR : gh-pages/docs/qa-ui-tests
271+ KEEP_DAYS : " 90"
272+ INPUT_TAGS : ${{ inputs.TAGS }}
273+ APK_VERSION : ${{ env.REAL_BUILD_NUMBER }}
274+ APK_NAME : ${{ env.NEW_APK_NAME }}
275+ run : bash scripts/qa_android_ui_tests/reporting.sh publish-allure-report
276+
277+ # Clean temporary artifacts on the runner, even after failures.
278+ - name : Cleanup (remove secrets + build outputs)
279+ if : always()
280+ env :
281+ ALLURE_RESULTS_DIR : ${{ runner.temp }}/allure-results
282+ ALLURE_RESULTS_MERGED_DIR : ${{ runner.temp }}/allure-results-merged
283+ ALLURE_REPORT_DIR : ${{ runner.temp }}/allure-report
284+ run : bash scripts/qa_android_ui_tests/reporting.sh cleanup-workspace
0 commit comments