-
Notifications
You must be signed in to change notification settings - Fork 286
800 lines (684 loc) · 33.5 KB
/
ui-test-vscuse-template.yml
File metadata and controls
800 lines (684 loc) · 33.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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
name: uitest-vscuse-template
#
# Triggers:
# 1. Manual trigger (workflow_dispatch)
on:
workflow_dispatch:
inputs:
test_plan:
description: "Comma-separated list of test plan to run (e.g. DA_Regenrate_Action, Message_Extension_py_Local_Debug)"
required: false
image_tag:
description: "Docker image tag to use (e.g., 'latest', 'CY251212stable')"
required: true
default: "latest"
vscuse_version:
description: "VSCUSE Python package version to use (e.g., 'latest', 'v0.2.47')"
required: false
type: string
default: "latest"
email-receiver:
description: "email notification receiver"
required: false
type: string
max_retries:
description: "Maximum number of retry attempts (1-20, default: 7)"
required: false
type: number
default: 7
schedule_trigger:
description: 'Whether the build is triggered by schedule'
type: boolean
default: false
permissions:
actions: read
jobs:
discover-test-plans:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
test-plans: ${{ steps.get-plans.outputs.plans }}
email-receiver: ${{ steps.set-email.outputs.email-receiver }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set email receiver
id: set-email
run: |
# Set email receiver based on trigger type
if [ "${{ github.event.inputs.schedule_trigger }}" == "true" ] || [ "${{ github.event_name }}" == "schedule" ]; then
echo "email-receiver=zhenjiao@microsoft.com;M365AgentsToolkitEngineerTeam@microsoft.com;teamsfxqa@microsoft.com" >> $GITHUB_OUTPUT
echo "Schedule trigger: Using default email receivers"
elif [ -n "${{ github.event.inputs.email-receiver }}" ]; then
echo "email-receiver=${{ github.event.inputs.email-receiver }}" >> $GITHUB_OUTPUT
echo "Using user-provided email receiver: ${{ github.event.inputs.email-receiver }}"
else
echo "email-receiver=" >> $GITHUB_OUTPUT
echo "No email receiver specified"
fi
- name: Get test plans
id: get-plans
run: |
if [ -z "${{ github.event.inputs.test_plan }}" ]; then
# Get only top-level JSON files from plans directory (strip the .json extension)
plans=$(find packages/tests/vscuse/vscode-test-cases/plans/ -maxdepth 1 -type f -name "*.json" ! -name "Feature_*" ! -name "Sample_*" -exec basename {} .json \; | jq -R -s -c 'split("\n")[:-1]')
echo "plans=$plans" >> $GITHUB_OUTPUT
echo "Found test plans: $plans"
else
# Use the input test_plan file name(s)
# Support comma-separated list, trim spaces
input_plans=$(echo "${{ github.event.inputs.test_plan }}" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | jq -R -s -c 'split("\n")[:-1]')
echo "plans=$input_plans" >> $GITHUB_OUTPUT
echo "Using input test plans: $input_plans"
fi
main:
name: Case-${{ matrix.test_plan }}
needs: discover-test-plans
runs-on: ubuntu-latest
timeout-minutes: 50
strategy:
matrix:
test_plan: ${{ fromJson(needs.discover-test-plans.outputs.test-plans) }}
fail-fast: false
max-parallel: 100
permissions:
contents: read
packages: write
id-token: write
security-events: write
environment: engineering
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.TEST_TENANT_AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_API_KEY: ${{ secrets.TEST_TENANT_AZURE_OPENAI_KEY }}
AZURE_OPENAI_MODEL: ${{ vars.TEST_TENANT_AZURE_OPENAI_MODEL }}
AZURE_OPENAI_API_VERSION: ${{ vars.TEST_TENANT_AZURE_OPENAI_API_VERSION }}
# AZURE AI search
AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME: "text-embedding-ada-002"
AZURE_SEARCH_ENDPOINT: ${{ secrets.AZURE_SEARCH_ENDPOINT }}
AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }}
# Azure Service Principal for M365 Toolkit authentication
#AZURE_CLIENT_ID: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID }}
#AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SERVICE_PRINCIPAL_SECRET }}
AZURE_TENANT_ID: ${{ secrets.TEST_TENANT_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.TEST_TENANT_SUBSCRIPTION_ID }}
AZURE_AUTH_ENABLED: ${{ secrets.AZURE_SERVICE_PRINCIPAL_ID != '' && secrets.AZURE_SERVICE_PRINCIPAL_SECRET != '' && secrets.TEST_TENANT_TENANT_ID != '' }}
# M365 account for testing (comma-separated list from GitHub variable)
M365_USERNAMES: ${{ vars.M365_USERNAMES }}
M365_ACCOUNT_PASSWORD: ${{ secrets.TEST_TENANT_M365_ACCOUNT_PASSWORD }}
# The test account for Copilot features
M365_ACCOUNT_NAME_EnableCopilotAccess: ${{ secrets.TEST_TENANT_M365_ACCOUNT_NAME }}
M365_ACCOUNT_PASSWORD_EnableCopilotAccess: ${{ secrets.TEST_TENANT_M365_ACCOUNT_PASSWORD }}
# M365 admin account with Global admin role
M365_ACCOUNT_ADMIN_ACCOUNT: ${{ vars.TEST_TENANT_M365_ACCOUNT_ADMIN_ACCOUNT }}
M365_ACCOUNT_ADMIN_PASSWORD: ${{ secrets.TEST_TENANT_M365_ACCOUNT_ADMIN_PASSWORD }}
# M365 account without copilot license
M365_ACCOUNT_NO_COPILOT_NAME: ${{ vars.TEST_TENANT_M365_ACCOUNT_NO_COPILOT_NAME }}
M365_ACCOUNT_NO_COPILOT_PASSWORD: ${{ secrets.TEST_TENANT_M365_ACCOUNT_NO_COPILOT_PASSWORD }}
AZURE_ACCOUNT_NAME: ${{ secrets.TEST_TENANT_AZURE_ACCOUNT_NAME }}
AZURE_ACCOUNT_PASSWORD: ${{ secrets.TEST_TENANT_AZURE_ACCOUNT_PASSWORD }}
M365_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }}
# GITHUB test account
GITHUB_ACCOUNT_NAME: ${{ secrets.GH_ACCOUNT_NAME }}
GITHUB_ACCOUNT_PASSWORD: ${{ secrets.GH_ACCOUNT_PASSWORD }}
GITHUB_MFA_SECRET: ${{ secrets.GH_MFA_SECRET }}
#Additional value:
DA_MCP_ENTRA_SSO_CLIENTID: ${{ secrets.DA_MCP_ENTRA_SSO_CLIENTID }}
DA_MCP_OAUTH_CLIENT_SECERT: ${{ secrets.DA_MCP_OAUTH_CLIENT_SECERT }}
DA_MCP_OAUTH_CLIENTID: ${{ secrets.DA_MCP_OAUTH_CLIENTID }}
# SQL server
SQL_USER: ${{ secrets.SQL_USER }}
SECRET_SQL_PASSWORD: ${{ secrets.SECRET_SQL_PASSWORD }}
SQL_SERVER: "vscuse-test-sql.database.windows.net"
SQL_DATABASE: "vscuse-test-db"
# reddit
REDDIT_ID: "${{secrets.REDDIT_ID}}"
SECRET_REDDIT_PASSWORD: ${{secrets.SECRET_REDDIT_PASSWORD}}
# Foundry related parameters
AZURE_AI_FOUNDRY_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_FOUNDRY_PROJECT_ENDPOINT }}
AGENT_ID: ${{ secrets.AGENT_ID }}
# ms account
MS_AZURE_ACCOUNT_NAME: ${{vars.MS_AZURE_ACCOUNT_NAME}}
MS_AZURE_ACCOUNT_PASSWORD: ${{secrets.MS_AZURE_ACCOUNT_PASSWORD}}
# Test AAD App info
TEST_AAD_APP_ID: ${{vars.TEST_AAD_APP_ID}}
TEST_AAD_APP_PASSWORD: ${{secrets.TEST_AAD_APP_PASSWORD}}
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 22
- name: Install dependencies for download actions
run: npm install @octokit/auth-app @octokit/request @octokit/core node-fetch
shell: bash
- name: Run download script
run: node download-vscuse-python.js "${{ github.event.inputs.vscuse_version || 'latest' }}" "${{ github.workspace }}/packages/tests/vscuse/vscode-test-cases"
shell: bash
working-directory: .github/actions/setup-vscuse-environment
- name: Set up Python virtual environment and install wheel
run: |
cd packages/tests/vscuse/vscode-test-cases
python -m venv .venv
source .venv/bin/activate
WHEEL_FILE=$(ls *.whl | head -n 1)
pip install "./$WHEEL_FILE"
shell: bash
- name: Verify Azure Configuration
run: |
echo "Verifying Azure configuration..."
# Check Azure OpenAI (required)
if [ -z "$AZURE_OPENAI_ENDPOINT" ] || [ -z "$AZURE_OPENAI_API_KEY" ]; then
echo "❌ Azure OpenAI configuration missing"
exit 1
else
echo "✅ Azure OpenAI configured"
fi
# Check Azure Service Principal (optional)
if [ -n "$AZURE_CLIENT_ID" ] && [ -n "$AZURE_CLIENT_SECRET" ] && [ -n "$AZURE_TENANT_ID" ]; then
echo "✅ Azure Service Principal configured - M365 Toolkit authentication enabled"
else
echo "⚠️ Azure Service Principal not configured - M365 Toolkit authentication disabled"
fi
- name: Set m365 account randomly
run: |
# Split comma-separated M365_USERNAMES into an array
IFS=',' read -ra users <<< "$M365_USERNAMES"
# Trim whitespace from each entry
for i in "${!users[@]}"; do
users[$i]=$(echo "${users[$i]}" | xargs)
done
count=${#users[@]}
index=$((RANDOM%$count))
echo "account index: $index, total accounts: $count"
echo "M365_ACCOUNT_NAME=${users[index]}" >> $GITHUB_ENV
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull pre-built Docker image
run: |
IMAGE_TAG="${{ github.event.inputs.image_tag }}"
if [ -z "$IMAGE_TAG" ]; then
IMAGE_TAG="latest"
fi
VSCUSE_VSCODE_IMAGE="ghcr.io/officedev/vscuse-atk-vscode:$IMAGE_TAG"
echo "VSCUSE_VSCODE_IMAGE=$VSCUSE_VSCODE_IMAGE" >> $GITHUB_ENV
echo "Pulling pre-built Docker image: $VSCUSE_VSCODE_IMAGE"
docker pull $VSCUSE_VSCODE_IMAGE
echo "✅ Image pulled successfully!"
echo "Image details:"
docker images $VSCUSE_VSCODE_IMAGE
- name: Run test
run: |
echo "🚀 Running test plan: ${{ matrix.test_plan }}"
echo "Using image tag: ${{ github.event.inputs.image_tag || 'latest' }}"
echo "Test execution started at: $(date)"
cd packages/tests/vscuse/vscode-test-cases
python -m venv .venv
source .venv/bin/activate
vscuse execute --config-file ./config.yaml --groups-dir groups ./plans/${{ matrix.test_plan }}.json
- name: Rename test_report.html to index.html
if: always()
run: |
REPORT_DIR="packages/tests/vscuse/vscode-test-cases/test_report"
if [ -f "$REPORT_DIR/test_report.html" ]; then
mv "$REPORT_DIR/test_report.html" "$REPORT_DIR/index.html"
echo "Renamed test_report.html to index.html"
else
echo "No test_report.html found to rename"
fi
- name: Encrypt and zip test report
if: always()
run: |
REPORT_DIR="packages/tests/vscuse/vscode-test-cases/test_report"
ZIP_FILE="test-report-${{ matrix.test_plan }}-${{ github.run_number }}.zip"
if [ -z "${{ secrets.ARTIFACT_ZIP_PASSWORD }}" ]; then
echo "❌ Error: ARTIFACT_ZIP_PASSWORD secret is not set"
echo "Please configure the ARTIFACT_ZIP_PASSWORD secret in repository settings"
exit 1
fi
# Install zip if not available
sudo apt-get update && sudo apt-get install -y zip
# Create encrypted zip file in the report directory
cd "$REPORT_DIR"
zip -e -P "${{ secrets.ARTIFACT_ZIP_PASSWORD }}" "$ZIP_FILE" index.html
cd -
echo "✅ Created encrypted zip: $REPORT_DIR/$ZIP_FILE"
echo "ZIP_FILE=$ZIP_FILE" >> $GITHUB_ENV
shell: bash
- name: Upload encrypted test report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-report-${{ matrix.test_plan }}-${{ github.run_number }}
path: packages/tests/vscuse/vscode-test-cases/test_report/${{ env.ZIP_FILE }}
retention-days: 7
if-no-files-found: ignore
# Upload files to Azure Blob Storage
- name : Login to Azure
if: always()
uses: azure/login@v2
with:
client-id: ${{secrets.DEVOPS_CLIENT_ID}}
tenant-id: ${{secrets.DEVOPS_TENANT_ID}}
subscription-id: ${{secrets.DEVOPS_SUB_ID}}
enable-AzPSSession: true
- name: 🌐Upload files to Azure Blob Storage
if: always()
shell: pwsh
run: |
$guid = [guid]::NewGuid().ToString()
$account = $env:AZURE_STORAGE_ACCOUNT
$sourcePath = "packages/tests/vscuse/vscode-test-cases/test_report/"
$destination = "`content/$guid"
$storageUrl = "https://storproxy-app-voazuxhhvtgiq.azurewebsites.net/$guid/index.html"
Write-Host "🌐Here is the report: $storageUrl"
echo "$storageUrl" > storage_url.txt
az storage blob upload-batch `
--account-name $account `
--auth-mode login `
--destination $destination `
--source "$sourcePath" `
--overwrite
env:
AZURE_STORAGE_ACCOUNT: storproxystvoazuxhhvtgiq
- name: Upload storage URL artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: storage-url-${{ matrix.test_plan }}-${{ github.run_id }}
path: storage_url.txt
retention-days: 7
if-no-files-found: ignore
- name: Cleanup
if: always()
run: |
echo "🧹 Cleaning up resources..."
# Stop and remove any containers using our image
echo "Cleaning up VSCode containers..."
docker stop $(docker ps -q --filter "ancestor=ghcr.io/officedev/vscuse-atk-vscode:${{ github.event.inputs.image_tag || 'latest' }}") 2>/dev/null || echo "No containers to stop"
docker rm $(docker ps -aq --filter "ancestor=ghcr.io/officedev/vscuse-atk-vscode:${{ github.event.inputs.image_tag || 'latest' }}") 2>/dev/null || echo "No containers to remove"
rerun:
continue-on-error: true
permissions:
actions: write
needs: main
if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.inputs.schedule_trigger == 'true') && failure() && github.run_attempt < (github.event.inputs.max_retries || 7) }}
runs-on: ubuntu-latest
env:
MAX_RETRIES: ${{ github.event.inputs.max_retries || 7 }}
steps:
- name: Calculate max retries
id: calc-retries
run: |
# Get max retries from input, default to 7, cap at 20
max_retries=${{ github.event.inputs.max_retries || 7 }}
if [ "$max_retries" -gt 20 ]; then
max_retries=20
fi
if [ "$max_retries" -lt 1 ]; then
max_retries=1
fi
echo "max_retries=$max_retries" >> $GITHUB_OUTPUT
echo "Using max retries: $max_retries"
- name: trigger rerun workflow
run: |
curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/rerun.yml/dispatches \
-d '{"ref":"${{ github.ref_name }}","inputs":{"run_id":"${{ github.run_id }}", "max_attempts":"${{ steps.calc-retries.outputs.max_retries }}"}}'
report:
# Only send email on the last attempt or when all tests pass
# Send email if: (1) triggered by schedule or schedule_trigger is true, OR (2) triggered manually with email-receiver input provided
if: ${{ always() && (github.event_name == 'schedule' || github.event.inputs.schedule_trigger == 'true' || needs.discover-test-plans.outputs.email-receiver != '') && (success() || github.run_attempt >= (github.event.inputs.max_retries || 7)) }}
needs: [discover-test-plans, main]
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/tests/vscuse
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Download storage URLs
uses: actions/download-artifact@v4
with:
pattern: storage-url-*
path: /tmp/storage-urls
- name: Process storage URLs
shell: bash
run: |
if [ -d "/tmp/storage-urls" ]; then
echo "Found storage URLs folder. Processing contents..."
# Find and extract all zip files to /tmp/storage-urls directory
find /tmp/storage-urls -name "*.zip" -exec unzip -o {} -d /tmp/storage-urls \;
# Create a JSON object from all storage_url.txt files
echo "{" > /tmp/url_mapping.json
first_entry=true
# Find all storage_url.txt files and process them
find /tmp/storage-urls -type f -name "storage_url.txt" | while read file; do
# Extract the test_plan and run_id from the parent directory name (storage-url-TESTPLAN-RUNID)
dir_name=$(basename $(dirname "$file"))
# Format: storage-url-{test_plan}-{run_id}
# Extract test_plan (everything between 'storage-url-' and the last '-{run_id}')
run_id=$(echo "$dir_name" | rev | cut -d'-' -f1 | rev)
test_plan=$(echo "$dir_name" | sed "s/^storage-url-//" | sed "s/-${run_id}$//")
case_run_id="${test_plan}-${run_id}"
echo "Processing: dir_name=$dir_name, test_plan=$test_plan, run_id=$run_id, case_run_id=$case_run_id"
# Read the content of the file
content=$(cat "$file" | tr -d '\n' | tr -d '\r')
# Add comma for all entries except the first one
if [ "$first_entry" = true ]; then
first_entry=false
else
echo "," >> /tmp/url_mapping.json
fi
# Add the entry to JSON (properly escaped)
echo " \"$case_run_id\": \"$content\"" >> /tmp/url_mapping.json
done
echo "}" >> /tmp/url_mapping.json
echo "Generated JSON mapping:"
cat /tmp/url_mapping.json
# Count total files processed
total_urls=$(find /tmp/storage-urls -type f -name "storage_url.txt" | wc -l)
echo "Total storage URL files processed: $total_urls"
else
echo "No storage URL artifacts found"
echo "{}" > /tmp/url_mapping.json
fi
- name: Install Dateutils
run: |
sudo apt install dateutils
- name: list jobs
id: list-jobs
working-directory: packages/tests/vscuse
env:
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
AZURE_DEVOPS_ORG: "msazure"
AZURE_DEVOPS_PROJECT: "Microsoft%20Teams%20Extensibility"
run: |
page=1
jobs="[]"
echo "Initial jobs: $jobs"
report_map_json_file_path="/tmp/url_mapping.json"
# Function to query Azure DevOps for bugs with specific tag
query_bugs_by_tag() {
local tag="$1"
local encoded_tag=$(echo "$tag" | sed 's/ /%20/g')
# WIQL query to find active bugs with the specific tag
local wiql_query="{\"query\": \"SELECT [System.Id], [System.Title], [System.State] FROM WorkItems WHERE [System.WorkItemType] = 'Bug' AND [System.State] IN ('Active','New','Committed') AND [System.Tags] CONTAINS '${tag}'\"}"
local http_code
local response
response=$(curl -s -w "\n%{http_code}" -u ":${AZURE_DEVOPS_PAT}" \
-H "Content-Type: application/json" \
-X POST \
"https://dev.azure.com/${AZURE_DEVOPS_ORG}/${AZURE_DEVOPS_PROJECT}/_apis/wit/wiql?api-version=7.0" \
-d "$wiql_query")
# Extract HTTP status code from the last line
http_code=$(echo "$response" | tail -n1)
# Remove the last line (status code) to get the body
response=$(echo "$response" | sed '$d')
if [ "$http_code" != "200" ]; then
echo "HTTP_ERROR:$http_code"
return 1
fi
# Validate that response is JSON
if ! echo "$response" | jq empty 2>/dev/null; then
echo "HTTP_ERROR:INVALID_JSON"
return 1
fi
echo "$response"
}
while :
do
url="https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}/jobs?per_page=100&page=$page"
echo "Fetching URL: $url"
resp=$(curl -sf -D /tmp/gh_headers_$page -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "$url")
new_jobs=$(echo "$resp" | jq -cr '.jobs')
new_jobs_count=$(echo "$resp" | jq -r '.jobs | length')
echo "Page $page: fetched $new_jobs_count jobs"
jobs=$(jq -cr --slurp 'add' <(echo "$jobs") <(echo "$new_jobs"))
# Check Link header from the same GET response for next page
has_next=$(grep -Fi "link:" /tmp/gh_headers_$page 2>/dev/null | grep "rel=\"next\"" || true)
rm -f /tmp/gh_headers_$page
if [ -z "$has_next" ]; then
break
fi
page=$((page+1))
done
total_jobs=$(echo "$jobs" | jq 'length')
echo "Total jobs fetched across all pages: $total_jobs"
echo "Final job list: $jobs"
cases=$(echo "$jobs" | jq -r '.[] | select(.name | contains("Case-")) | .name')
echo "Extracted case names: $cases"
passed=0
failed=0
failedlimit=100
passedlimit=100
failedlist=""
passedlist=""
lists=""
emails="${{ needs.discover-test-plans.outputs.email-receiver }}"
echo "Initial email list: $emails"
while IFS= read -r case;
do
if [ -z "$case" ]; then
continue
fi
echo "Processing case: $case"
name=$(echo "$case" | awk -F '|' '{print $1}')
case_id=$(echo "$name" | awk -F '-' '{print $2}')
os=$(echo "$case" | awk -F '|' '{print $2}')
node=$(echo "$case" | awk -F '|' '{print $3}')
branch=$(echo "$case" | awk -F '|' '{print $4}')
title="Unknown Test Case Title"
author="N/A"
work_item_url="N/A"
owner="N/A"
# Extract the title from the plan JSON file
plan_file="./vscode-test-cases/plans/${case_id}.json"
echo "Looking for plan file: $plan_file"
if [ -f "$plan_file" ]; then
echo "Found plan file: $plan_file"
# Get title from plan_metadata.name
title=$(jq -r '.plan_metadata.name // "Unknown Test Case Title"' "$plan_file")
echo "Extracted title: $title"
# Get owner from plan_metadata.description.owner
owner=$(jq -r '.plan_metadata.description.owner // "N/A"' "$plan_file")
echo "Extracted owner: $owner"
# Get work item IDs from plan_metadata.description.workitem
# Workitem may contain multiple work item IDs separated by comma, space, or semicolon
workitem=$(jq -r '.plan_metadata.description.workitem // ""' "$plan_file")
if [ -n "$workitem" ] && [ "$workitem" != "null" ]; then
# Split by comma, space, or semicolon and process each work item ID
work_item_links=""
# Replace commas and semicolons with spaces, then iterate
for item_id in $(echo "$workitem" | tr ',;' ' '); do
# Trim whitespace
item_id=$(echo "$item_id" | xargs)
if [ -n "$item_id" ]; then
# If it looks like a work item ID (numeric), create a link
if [[ "$item_id" =~ ^[0-9]+$ ]]; then
if [ -n "$work_item_links" ]; then
work_item_links="$work_item_links, "
fi
work_item_links="$work_item_links<a href=\\\"https://msazure.visualstudio.com/Microsoft%20Teams%20Extensibility/_workitems/edit/$item_id\\\">$item_id</a>"
fi
fi
done
if [ -n "$work_item_links" ]; then
work_item_url="$work_item_links"
else
work_item_url="$workitem"
fi
fi
echo "Extracted work items: $work_item_url"
else
echo "Plan file not found: $plan_file"
# Fallback: use case_id as title with underscores replaced by spaces
title=$(echo "$case_id" | tr '_' ' ')
fi
echo "Extracted info - Name: $name, OS: $os, Node: $node, Branch: $branch, Case ID: $case_id, Title: $title, Author: $author, Work Item URL: $work_item_url"
# file=$(find src -name "$name.test.ts" 2>/dev/null)
# echo "Test file found: $file"
status=$(echo "$jobs" | jq --arg case "$case" -r '.[] | select(.name == $case ) | .conclusion')
echo "Job status: $status"
run_id=$(echo "$jobs" | jq --arg case "$case" -r '.[] | select(.name == $case ) | .run_id')
echo "Run Id: $run_id"
# Combine the case name and run_id to create a lookup key
lookup_key="${case_id}-${run_id}"
echo "Lookup Key: $lookup_key"
# Get the report URL from the JSON file using the lookup_key
report_url=$(jq -r --arg key "$lookup_key" '.[$key]' "$report_map_json_file_path")
if [ -z "$report_url" ] || [ "$report_url" = "null" ]; then
report_url="N/A"
else
report_url="<a href=\\\"$report_url\\\">Report</a>"
fi
echo "Report URL: $report_url"
if [[ -n "$email" && ! "$emails" == *"$email"* && "$status" == "failure" ]]; then
emails="$emails;$email;"
fi
echo "Updated email list: $emails"
started_at=$(echo "$jobs" | jq --arg case "$case" -r '.[] | select(.name == $case ) | .started_at')
completed_at=$(echo "$jobs" | jq --arg case "$case" -r '.[] | select(.name == $case ) | .completed_at')
echo "Start time: $started_at, Completed time: $completed_at"
duration=$(dateutils.ddiff "$started_at" "$completed_at" -f "%Mm %Ss" 2>/dev/null)
echo "Calculated duration: $duration"
label=""
if [ "$status" == "success" ]; then
passed=$((passed+1))
label="<span style=\\\"background-color:#2aa198;color:white;font-weight:bold;\\\">PASSED</span>"
else
failed=$((failed+1))
label="<span style=\\\"background-color: #dc322f;color:white;font-weight:bold;\\\">FAILED</span>"
fi
echo "Job result label: $label"
url=$(echo "$jobs" | jq --arg case "$case" -r '.[] | select(.name == $case ) | .html_url')
display_name=$(echo "$name" | sed 's/^Case-//')
url="<a href=\\\"$url\\\">$display_name</a>"
echo "Job URL: $url"
linked_bugs=""
# Query Azure DevOps for linked bugs if the case failed
if [ "$status" != "success" ]; then
echo "Case failed, querying Azure DevOps for bugs with tag: $display_name"
# Try to query Azure DevOps, with error handling
bug_response=""
query_error=""
if [ -z "$AZURE_DEVOPS_PAT" ]; then
query_error="⚠️No ADO PAT configured"
echo "Azure DevOps PAT not configured, skipping bug query"
else
bug_response=$(query_bugs_by_tag "$display_name" 2>/dev/null)
query_exit_code=$?
if [ $query_exit_code -ne 0 ]; then
# Extract HTTP error code if available
if echo "$bug_response" | grep -q "^HTTP_ERROR:"; then
error_detail=$(echo "$bug_response" | sed 's/^HTTP_ERROR://')
query_error="⚠️ADO API error (HTTP $error_detail)"
echo "Azure DevOps API error: HTTP $error_detail"
else
query_error="⚠️Network issue"
echo "Azure DevOps query failed with exit code $query_exit_code"
fi
else
echo "Bug query returned valid JSON response"
# Check for API-level errors in JSON response
api_error=$(echo "$bug_response" | jq -r '.message // empty' 2>/dev/null)
if [ -n "$api_error" ]; then
query_error="⚠️ADO API error"
echo "Azure DevOps API error: $api_error"
fi
fi
fi
if [ -n "$query_error" ]; then
linked_bugs="$query_error"
echo "Bug query failed: $query_error"
else
# Extract work item IDs from the response
bug_ids=$(echo "$bug_response" | jq -r '.workItems[]?.id // empty' 2>/dev/null)
if [ -n "$bug_ids" ]; then
bug_links=""
for bug_id in $bug_ids; do
if [ -n "$bug_links" ]; then
bug_links="$bug_links, "
fi
bug_links="$bug_links<a href=\\\"https://dev.azure.com/${AZURE_DEVOPS_ORG}/${AZURE_DEVOPS_PROJECT}/_workitems/edit/$bug_id\\\">🐞$bug_id</a>"
done
linked_bugs="$bug_links"
echo "Found linked bugs: $linked_bugs"
else
linked_bugs="❌Failed but no bug yet"
echo "No linked bugs found for failed case"
fi
fi
fi
row="<tr> <td style=\\\"text-align: left;\\\">$url</td> <td style=\\\"text-align: left;\\\">$title</td> <td style=\\\"text-align: center;\\\">$label</td> <td style=\\\"text-align: center;\\\">$owner</td> <td style=\\\"text-align: center;\\\">$work_item_url</td> <td style=\\\"text-align: center;\\\">$linked_bugs</td> <td style=\\\"text-align: center;\\\">$duration</td> <td style=\\\"text-align: center;\\\">$report_url</td> </tr>"
echo "Generated row: $row"
if [[ "$status" == "success" && $passed -lt $passedlimit ]]; then
passedlist="$passedlist $row"
elif [[ "$status" != "success" && $failed -lt $failedlimit ]]; then
failedlist="$failedlist $row"
fi
done <<< "$cases"
lists="$failedlist $passedlist"
echo "Final failed list: $failedlist"
echo "Final passed list: $passedlist"
body="<a href=\\\"https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\\\">Test report link.</a> <br/> <table class=\\\"w3-table w3-striped w3-bordered\\\"> <tr> <th>CASE</th> <th>TITLE</th> <th>STATUS</th> <th>OWNER</th> <th>WORK ITEM</th> <th>LINKED BUG(S)</th> <th>DURATION</th> <th>REPORT</th> </tr> $lists </table> <br />"
echo "Generated email body: $body"
total=$((passed+failed))
echo "Total jobs: $total, Passed: $passed, Failed: $failed"
subject="Vsc Use UI Test Report[Templates] ($passed/$total Passed)"
if [ $failed -gt 0 ]; then
subject="[FAILED] $subject"
else
subject="[PASSED] $subject"
fi
echo "Final subject: $subject"
echo "body=$body" >> $GITHUB_OUTPUT
echo "to=$emails" >> $GITHUB_OUTPUT
echo "subject=$subject" >> $GITHUB_OUTPUT
- name: Prepare email body file
if: always()
run: |
printf '%s' "${{ steps.list-jobs.outputs.body }}" > email-body.html
echo "BODY_FILE=$(pwd)/email-body.html" >> $GITHUB_ENV
- name: Send E-mail
uses: ./.github/actions/send-email-report-vscuse
env:
TO: ${{ steps.list-jobs.outputs.to }}
SUBJECT: ${{ steps.list-jobs.outputs.subject }}
MAIL_CLIENT_ID: ${{ secrets.TEST_CLEAN_CLIENT_ID }}
MAIL_CLIENT_SECRET: ${{ secrets.TEST_CLEAN_CLIENT_SECRET }}
MAIL_TENANT_ID: ${{ secrets.TEST_CLEAN_TENANT_ID }}
trigger-ui-test-others:
needs: report
if: ${{ always() && needs.report.result != 'skipped' && github.event.inputs.schedule_trigger == 'true'}}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Trigger UI Test Others Workflow
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run ui-test-vscuse-others.yml \
--repo ${{ github.repository }} \
--ref ${{ github.ref }} \
-f image_tag=${{ github.event.inputs.image_tag || 'latest' }} \
-f vscuse_version=${{ github.event.inputs.vscuse_version || 'latest' }} \
-f schedule_trigger=${{ github.event.inputs.schedule_trigger || 'false' }}