Skip to content

Commit 3f7ebc0

Browse files
Merge pull request #986 from ArmDeveloperEcosystem/prod-smoke-final
Finalize Arm smoke test production results
2 parents 2dfbb4f + 9cac207 commit 3f7ebc0

1,263 files changed

Lines changed: 63296 additions & 35804 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/apt-bootstrap/bootstrap.sh

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,29 @@ fi
104104
install_cmd+=("${package_array[@]}")
105105

106106
if [[ "$QUIET" == "true" ]]; then
107-
"${install_cmd[@]}" >/dev/null
107+
attempt=1
108+
backoff="$INITIAL_BACKOFF_SECONDS"
109+
until "${install_cmd[@]}" >/dev/null; do
110+
if (( attempt >= RETRIES )); then
111+
echo "apt-get install failed after ${RETRIES} attempts" >&2
112+
exit 100
113+
fi
114+
echo "apt-get install failed on attempt ${attempt}; retrying in ${backoff}s" >&2
115+
sleep "$backoff"
116+
attempt=$((attempt + 1))
117+
backoff=$((backoff * 2))
118+
done
108119
else
109-
"${install_cmd[@]}"
120+
attempt=1
121+
backoff="$INITIAL_BACKOFF_SECONDS"
122+
until "${install_cmd[@]}"; do
123+
if (( attempt >= RETRIES )); then
124+
echo "apt-get install failed after ${RETRIES} attempts" >&2
125+
exit 100
126+
fi
127+
echo "apt-get install failed on attempt ${attempt}; retrying in ${backoff}s" >&2
128+
sleep "$backoff"
129+
attempt=$((attempt + 1))
130+
backoff=$((backoff * 2))
131+
done
110132
fi

.github/actions/collect-batch-results/action.yml

Lines changed: 193 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -255,20 +255,106 @@ runs:
255255
lowered = str(name or "").strip().lower()
256256
return lowered.startswith("test 6") or "regression" in lowered
257257
258+
ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
259+
260+
def strip_ansi(value):
261+
return ANSI_RE.sub("", str(value or ""))
262+
263+
def fetch_job_log(job):
264+
if not isinstance(job, dict):
265+
return ""
266+
job_id = str(job.get("id") or "").strip()
267+
if not job_id or not github_token:
268+
return ""
269+
try:
270+
return subprocess.check_output(
271+
["gh", "run", "view", current_run_id, "--repo", repository, "--log", "--job", job_id],
272+
text=True,
273+
stderr=subprocess.DEVNULL,
274+
env={**os.environ, "GH_TOKEN": github_token},
275+
)
276+
except Exception:
277+
return ""
278+
279+
def extract_summary_statuses_from_log(log_text):
280+
statuses = {}
281+
for raw_line in str(log_text or "").splitlines():
282+
line = strip_ansi(raw_line)
283+
match = re.search(
284+
r'echo\s+"([1-6])\.\s+[^"]*?:\s*(passed|failed|skipped)"',
285+
line,
286+
re.IGNORECASE,
287+
)
288+
if not match:
289+
match = re.search(
290+
r"\b([1-6])\.\s+[^:\n]*?:\s*(passed|failed|skipped)\b",
291+
line,
292+
re.IGNORECASE,
293+
)
294+
if match:
295+
statuses[int(match.group(1))] = match.group(2).lower()
296+
return statuses
297+
298+
def detail_test_number(detail, fallback_index):
299+
name = str(detail.get("name") or "")
300+
lowered = name.lower()
301+
match = re.search(r"\btest\s*([1-6])\b", lowered)
302+
if match:
303+
return int(match.group(1))
304+
if lowered.startswith("regression applicability") or "regression" in lowered:
305+
return 6
306+
return fallback_index
307+
308+
def apply_summary_log_statuses(details, summary_statuses):
309+
if not summary_statuses:
310+
return details
311+
updated = []
312+
test_index = 0
313+
for detail in details:
314+
if is_test_detail_step(detail.get("name")):
315+
test_index += 1
316+
test_number = detail_test_number(detail, test_index)
317+
if test_number in summary_statuses:
318+
detail = {**detail, "status": summary_statuses[test_number]}
319+
updated.append(detail)
320+
return updated
321+
322+
def job_has_failed_step(job, step_name):
323+
target = str(step_name or "").strip().lower()
324+
if not isinstance(job, dict) or not target:
325+
return False
326+
for step in job.get("steps") or []:
327+
name = str(step.get("name") or "").strip().lower()
328+
conclusion = str(step.get("conclusion") or "").strip().lower()
329+
if name == target and conclusion == "failure":
330+
return True
331+
return False
332+
333+
def mark_core_details_failed(details):
334+
updated = []
335+
for detail in details:
336+
if is_test_detail_step(detail.get("name")) and not is_regression_detail_name(detail.get("name")):
337+
detail = {**detail, "status": "failed"}
338+
updated.append(detail)
339+
return updated
340+
258341
def extract_test_details(job, regression_details):
259342
details = []
260343
if isinstance(job, dict):
344+
job_url = str(job.get("html_url") or "").strip()
261345
for step in job.get("steps") or []:
262346
name = str(step.get("name") or "").strip()
263347
if not is_test_detail_step(name):
264348
continue
265-
details.append(
266-
{
267-
"name": name,
268-
"status": normalize_detail_status(step.get("conclusion")),
269-
"duration_seconds": step_duration_seconds(step),
270-
}
271-
)
349+
detail = {
350+
"name": name,
351+
"status": normalize_detail_status(step.get("conclusion")),
352+
"duration_seconds": step_duration_seconds(step),
353+
}
354+
step_number = as_int(step.get("number"), 0)
355+
if job_url and step_number > 0:
356+
detail["url"] = f"{job_url}#step:{step_number}:1"
357+
details.append(detail)
272358
273359
if regression_details:
274360
regression_entry = dict(regression_details[0])
@@ -310,6 +396,8 @@ runs:
310396
core_failed += 1
311397
else:
312398
skipped += 1
399+
if not is_regression_detail_name(name):
400+
core_failed += 1
313401
314402
return {
315403
"passed": passed,
@@ -372,12 +460,34 @@ runs:
372460
return "skipped"
373461
return "skipped"
374462
375-
def normalize_run_status(raw_status, tests_passed, tests_failed, tests_skipped, core_failed):
463+
def is_safe_regression_skip(decision):
464+
return str(decision or "").strip() in {
465+
"not_applicable_package_manager",
466+
"package_manager_regression_safe_skip",
467+
"no_newer_stable_available",
468+
"current_is_latest_stable",
469+
"pinned_is_latest",
470+
"already_current",
471+
"already_at_latest",
472+
"at_latest",
473+
"current_is_latest",
474+
"metadata_review_required",
475+
"runtime_validation_not_automated",
476+
"bounded_runtime_deferred",
477+
"arm64_desktop_artifact_unavailable",
478+
"hold_current_arm_dependency_gap",
479+
}
480+
481+
def normalize_run_status(raw_status, tests_passed, tests_failed, tests_skipped, core_failed, regression_decision):
376482
status = str(raw_status or "").strip().lower()
377483
if tests_failed > 0:
378484
return "failure"
379485
if core_failed > 0:
380486
return "failure"
487+
if tests_skipped > 0 and not (
488+
tests_skipped == 1 and is_safe_regression_skip(regression_decision)
489+
):
490+
return "failure"
381491
if tests_passed > 0:
382492
return "success"
383493
if tests_skipped > 0:
@@ -389,12 +499,16 @@ runs:
389499
def default_regression_result(status, decision):
390500
if decision == "not_applicable_package_manager":
391501
return "Regression validation not applicable: tested package installed via package manager in Tests 1-5"
502+
if decision == "package_manager_regression_safe_skip":
503+
return "Regression validation not applicable: package-manager installed current version"
392504
if decision == "next_install_validated":
393505
return "Next version installed successfully on Arm64"
394506
if decision == "next_bundle_validated":
395507
return "Next bundle validated successfully on Arm64"
396508
if decision == "next_image_validated":
397509
return "Next image validated successfully on Arm64"
510+
if decision == "limited_cpu_smoke_validated":
511+
return "Limited CPU-side proof passed on Arm64"
398512
if decision == "next_install_failed":
399513
return "Next version install failed on Arm64"
400514
if decision in {
@@ -412,6 +526,14 @@ runs:
412526
return "Tests 1-5 baseline must be older than the latest stable release"
413527
if decision == "not_configured":
414528
return "Regression validation not configured for this workflow"
529+
if decision == "runtime_validation_not_automated":
530+
return "Package-specific runtime validation is not automated"
531+
if decision == "bounded_runtime_deferred":
532+
return "Next-version bounded runtime validation deferred"
533+
if decision == "arm64_desktop_artifact_unavailable":
534+
return "Arm64 desktop artifact runtime validation is not available on the generic runner"
535+
if decision == "hold_current_arm_dependency_gap":
536+
return "Regression validation deferred because the Arm dependency gap blocks a meaningful next-version check"
415537
if status == "passed":
416538
return "Regression validation passed"
417539
if status == "failed":
@@ -469,14 +591,18 @@ runs:
469591
"at_latest",
470592
"current_is_latest",
471593
"no_newer_stable_available",
594+
"runtime_validation_not_automated",
595+
"bounded_runtime_deferred",
596+
"arm64_desktop_artifact_unavailable",
597+
"hold_current_arm_dependency_gap",
472598
}:
473599
latest_version = current_version
474-
if not latest_version and decision == "not_applicable_package_manager":
600+
if not latest_version and decision in {"not_applicable_package_manager", "package_manager_regression_safe_skip"}:
475601
latest_version = "not_applicable"
476602
if not latest_version:
477603
latest_version = "unknown"
478604
next_installed_version = meaningful_version(outputs.get("regression_next_installed_version")) or "not_installed"
479-
if decision == "not_applicable_package_manager" and next_installed_version == "not_installed":
605+
if decision in {"not_applicable_package_manager", "package_manager_regression_safe_skip"} and next_installed_version == "not_installed":
480606
next_installed_version = "not_applicable"
481607
regression_result = str(outputs.get("regression_result") or "").strip()
482608
if not regression_result:
@@ -560,6 +686,34 @@ runs:
560686
if regression_policy_info["applicability"] == "applicable":
561687
regression_details.append(regression_detail(outputs, regression_policy_info))
562688
test_details = extract_test_details(resolved_job, regression_details)
689+
initial_detail_counts = summarize_detail_counts(test_details)
690+
initial_detail_total = (
691+
initial_detail_counts["passed"]
692+
+ initial_detail_counts["failed"]
693+
+ initial_detail_counts["skipped"]
694+
)
695+
emitted_total = emitted_tests_passed + emitted_tests_failed + emitted_tests_skipped
696+
if (
697+
initial_detail_total > 0
698+
and emitted_total > 0
699+
and (
700+
emitted_tests_failed > initial_detail_counts["failed"]
701+
or emitted_core_failed > initial_detail_counts["core_failed"]
702+
or emitted_tests_passed != initial_detail_counts["passed"]
703+
or emitted_tests_skipped != initial_detail_counts["skipped"]
704+
)
705+
):
706+
summary_statuses = extract_summary_statuses_from_log(fetch_job_log(resolved_job))
707+
test_details = apply_summary_log_statuses(test_details, summary_statuses)
708+
709+
current_detail_counts = summarize_detail_counts(test_details)
710+
if (
711+
initial_detail_total > 0
712+
and str((resolved_job or {}).get("conclusion") or "").strip().lower() == "failure"
713+
and job_has_failed_step(resolved_job, "Calculate test summary")
714+
and current_detail_counts["core_failed"] == 0
715+
):
716+
test_details = mark_core_details_failed(test_details)
563717
regression_detail_entry = next(
564718
(detail for detail in test_details if is_regression_detail_name(detail.get("name"))),
565719
None,
@@ -601,6 +755,10 @@ runs:
601755
use_detail_counts = True
602756
elif tests_passed != detail_counts["passed"]:
603757
use_detail_counts = True
758+
elif tests_skipped != detail_counts["skipped"]:
759+
use_detail_counts = True
760+
elif emitted_total != detail_total:
761+
use_detail_counts = True
604762
elif core_failed != detail_counts["core_failed"]:
605763
use_detail_counts = True
606764
@@ -620,15 +778,28 @@ runs:
620778
str(detail.get("status") or "").strip().lower() == "failed"
621779
for detail in test_details
622780
)
781+
safe_single_regression_skip = (
782+
tests_failed == 0
783+
and core_failed == 0
784+
and tests_skipped == 1
785+
and is_safe_regression_skip(regression_decision_for_counts)
786+
)
787+
safe_package_manager_skip = (
788+
tests_failed == 0
789+
and core_failed == 0
790+
and tests_skipped == 1
791+
and regression_policy_info["reason"] == "package_manager_installed"
792+
)
623793
624-
if job_failed and not failed_detail_exists:
794+
if job_failed and not failed_detail_exists and not (safe_single_regression_skip or safe_package_manager_skip):
625795
promoted_regression_failure = False
626796
if isinstance(regression_detail_entry, dict):
627797
regression_decision = str(regression_detail_entry.get("decision") or "").strip()
628798
successful_regression_decisions = {
629799
"next_install_validated",
630800
"next_bundle_validated",
631801
"next_image_validated",
802+
"limited_cpu_smoke_validated",
632803
}
633804
if regression_decision not in {
634805
"baseline_failed",
@@ -675,7 +846,17 @@ runs:
675846
tests_skipped = detail_counts["skipped"]
676847
core_failed = detail_counts["core_failed"]
677848
678-
run_status = normalize_run_status(raw_run_status, tests_passed, tests_failed, tests_skipped, core_failed)
849+
regression_decision_for_status = regression_decision_for_counts or str(outputs.get("regression_decision") or "").strip()
850+
if not regression_decision_for_status and regression_policy_info["reason"] == "package_manager_installed":
851+
regression_decision_for_status = "not_applicable_package_manager"
852+
run_status = normalize_run_status(
853+
raw_run_status,
854+
tests_passed,
855+
tests_failed,
856+
tests_skipped,
857+
core_failed,
858+
regression_decision_for_status,
859+
)
679860
badge_status = "passing" if run_status == "success" else "failing"
680861
681862
result_payload = {

0 commit comments

Comments
 (0)