Persist Currents run URLs for ATVM watcher notes
This commit is contained in:
@@ -259,6 +259,15 @@ This file stores run-specific examples only when a run produced a new learning r
|
|||||||
- Action for future runs:
|
- Action for future runs:
|
||||||
- Render ATVM status output in that section order for both local output and Mattermost posts.
|
- Render ATVM status output in that section order for both local output and Mattermost posts.
|
||||||
|
|
||||||
|
## Run Learning: 2026-03-27 (Persist the Currents run URL outside the transient runner log)
|
||||||
|
- Observed failure mode:
|
||||||
|
- The watcher can include the Currents run URL in `NOTES:`, but only if it can still read the URL from live runner output or a consolidated run log.
|
||||||
|
- In practice, `/tmp/<build-name>.log` is not guaranteed to exist, and the host reporter artifacts do not preserve the final Currents run URL.
|
||||||
|
- Action for future runs:
|
||||||
|
- Persist the Currents `Recorded Run` URL as soon as `run-sorry-cypress.py` sees it.
|
||||||
|
- Store it under the watcher state directory for the parent build so it survives runner exit and missing log files.
|
||||||
|
- Prefer the persisted Currents URL store over transient log scraping when building the final `NOTES:` section.
|
||||||
|
|
||||||
## Run Learning: 2026-03-27 (Default ATVM approval should include the watcher)
|
## Run Learning: 2026-03-27 (Default ATVM approval should include the watcher)
|
||||||
- Observed requirement:
|
- Observed requirement:
|
||||||
- The operator wants `approve` to mean run with watcher by default.
|
- The operator wants `approve` to mean run with watcher by default.
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ Use this as the default ATVM automation run-status template for:
|
|||||||
- `⏭️ SKIP`
|
- `⏭️ SKIP`
|
||||||
- Keep `Detail` concise.
|
- Keep `Detail` concise.
|
||||||
- Put broader context under `NOTES:`, not in the host table.
|
- Put broader context under `NOTES:`, not in the host table.
|
||||||
|
- When available, put the persistent Currents run URL in `NOTES:` so operators can open the exact recorded run directly.
|
||||||
- `COVERAGE:` should describe what the run was intended to cover without listing target hosts.
|
- `COVERAGE:` should describe what the run was intended to cover without listing target hosts.
|
||||||
- `TEST FLOW:` should describe the template-specific numbered run flow once for the whole test, not per host.
|
- `TEST FLOW:` should describe the template-specific numbered run flow once for the whole test, not per host.
|
||||||
- The watcher resolves `TEST FLOW:` from the run template name.
|
- The watcher resolves `TEST FLOW:` from the run template name.
|
||||||
|
|||||||
@@ -215,6 +215,39 @@ def write_state(state_file: Path, state: Dict[str, object]) -> None:
|
|||||||
state_file.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
|
state_file.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def load_currents_store(build_dir: Path) -> Dict[str, object]:
|
||||||
|
store_path = build_dir / "currents_urls.json"
|
||||||
|
if not store_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
loaded = json.loads(store_path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
return loaded if isinstance(loaded, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def latest_currents_url(build_dir: Path) -> Optional[str]:
|
||||||
|
store = load_currents_store(build_dir)
|
||||||
|
latest_url = store.get("latest_url")
|
||||||
|
return latest_url if isinstance(latest_url, str) and latest_url else None
|
||||||
|
|
||||||
|
|
||||||
|
def persisted_currents_url_for_build(build_dir: Path, build_id: Optional[str]) -> Optional[str]:
|
||||||
|
store = load_currents_store(build_dir)
|
||||||
|
by_build_id = store.get("by_build_id")
|
||||||
|
if not isinstance(by_build_id, dict):
|
||||||
|
return latest_currents_url(build_dir)
|
||||||
|
if build_id:
|
||||||
|
entry = by_build_id.get(build_id)
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
url = entry.get("url")
|
||||||
|
if isinstance(url, str) and url:
|
||||||
|
return url
|
||||||
|
if isinstance(entry, str) and entry:
|
||||||
|
return entry
|
||||||
|
return latest_currents_url(build_dir)
|
||||||
|
|
||||||
|
|
||||||
def parse_xml_timestamp(raw: Optional[str]) -> Optional[datetime]:
|
def parse_xml_timestamp(raw: Optional[str]) -> Optional[datetime]:
|
||||||
if not raw:
|
if not raw:
|
||||||
return None
|
return None
|
||||||
@@ -811,7 +844,7 @@ def split_log_segments(log_text: str, parent_build_name: str, categorized: bool,
|
|||||||
started_at=default_started_at,
|
started_at=default_started_at,
|
||||||
expected_hosts=extract_expected_hosts(log_text),
|
expected_hosts=extract_expected_hosts(log_text),
|
||||||
completed=False,
|
completed=False,
|
||||||
currents_url=extract_currents_url(log_text),
|
currents_url=None,
|
||||||
notes=[],
|
notes=[],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -828,7 +861,7 @@ def split_log_segments(log_text: str, parent_build_name: str, categorized: bool,
|
|||||||
started_at=default_started_at,
|
started_at=default_started_at,
|
||||||
expected_hosts=extract_expected_hosts(log_text),
|
expected_hosts=extract_expected_hosts(log_text),
|
||||||
completed=False,
|
completed=False,
|
||||||
currents_url=extract_currents_url(log_text),
|
currents_url=None,
|
||||||
notes=["Categorized mode was requested but no sub-run segment has appeared in the log yet."],
|
notes=["Categorized mode was requested but no sub-run segment has appeared in the log yet."],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -848,7 +881,7 @@ def split_log_segments(log_text: str, parent_build_name: str, categorized: bool,
|
|||||||
started_at=start_ts or default_started_at,
|
started_at=start_ts or default_started_at,
|
||||||
expected_hosts=expected_hosts,
|
expected_hosts=expected_hosts,
|
||||||
completed=index < len(segment_starts),
|
completed=index < len(segment_starts),
|
||||||
currents_url=extract_currents_url(segment_text),
|
currents_url=None,
|
||||||
notes=[f"Categorized sub-run {index} of {len(segment_starts)}."],
|
notes=[f"Categorized sub-run {index} of {len(segment_starts)}."],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -859,11 +892,13 @@ def evaluate_subrun(
|
|||||||
subrun: SubRun,
|
subrun: SubRun,
|
||||||
reporter_root: Path,
|
reporter_root: Path,
|
||||||
inventory: Dict[str, str],
|
inventory: Dict[str, str],
|
||||||
|
build_dir: Path,
|
||||||
end_boundary: Optional[datetime],
|
end_boundary: Optional[datetime],
|
||||||
parent_active: bool,
|
parent_active: bool,
|
||||||
cancelled: bool,
|
cancelled: bool,
|
||||||
) -> Tuple[str, Dict[str, HostResult], Optional[datetime], Optional[datetime], Optional[str], List[str]]:
|
) -> Tuple[str, Dict[str, HostResult], Optional[datetime], Optional[datetime], Optional[str], List[str]]:
|
||||||
notes = list(subrun.notes)
|
notes = list(subrun.notes)
|
||||||
|
currents_url = subrun.currents_url or persisted_currents_url_for_build(build_dir, subrun.display_name)
|
||||||
host_results = collect_host_results(
|
host_results = collect_host_results(
|
||||||
reporter_root=reporter_root,
|
reporter_root=reporter_root,
|
||||||
expected_hosts=subrun.expected_hosts,
|
expected_hosts=subrun.expected_hosts,
|
||||||
@@ -881,17 +916,17 @@ def evaluate_subrun(
|
|||||||
|
|
||||||
if cancelled:
|
if cancelled:
|
||||||
notes.append("Cancellation marker detected.")
|
notes.append("Cancellation marker detected.")
|
||||||
return "CANCELLED", host_results, start_ts, end_ts, subrun.currents_url, notes
|
return "CANCELLED", host_results, start_ts, end_ts, currents_url, notes
|
||||||
|
|
||||||
if subrun.completed:
|
if subrun.completed:
|
||||||
if not host_results:
|
if not host_results:
|
||||||
notes.append("This categorized sub-run ended but no host results were detected.")
|
notes.append("This categorized sub-run ended but no host results were detected.")
|
||||||
return "UNKNOWN", host_results, start_ts, end_ts, subrun.currents_url, notes
|
return "UNKNOWN", host_results, start_ts, end_ts, currents_url, notes
|
||||||
notes.append("Categorized sub-run completed and the next grouped run was launched.")
|
notes.append("Categorized sub-run completed and the next grouped run was launched.")
|
||||||
if check_end:
|
if check_end:
|
||||||
notes.append("Final `check-xml-files.ts` validation passed.")
|
notes.append("Final `check-xml-files.ts` validation passed.")
|
||||||
state = "FAILED" if any(result.failures for result in host_results.values()) else "COMPLETED"
|
state = "FAILED" if any(result.failures for result in host_results.values()) else "COMPLETED"
|
||||||
return state, host_results, start_ts, end_ts, subrun.currents_url, notes
|
return state, host_results, start_ts, end_ts, currents_url, notes
|
||||||
|
|
||||||
if parent_active:
|
if parent_active:
|
||||||
current_host = next((host for host in subrun.expected_hosts if host not in host_results), None)
|
current_host = next((host for host in subrun.expected_hosts if host not in host_results), None)
|
||||||
@@ -902,7 +937,7 @@ def evaluate_subrun(
|
|||||||
status="RUN",
|
status="RUN",
|
||||||
detail="in progress",
|
detail="in progress",
|
||||||
)
|
)
|
||||||
return "RUNNING", host_results, start_ts, end_ts, subrun.currents_url, notes
|
return "RUNNING", host_results, start_ts, end_ts, currents_url, notes
|
||||||
|
|
||||||
if check_end and not host_results:
|
if check_end and not host_results:
|
||||||
latest_host = collect_latest_host_reporter_artifact(
|
latest_host = collect_latest_host_reporter_artifact(
|
||||||
@@ -924,14 +959,15 @@ def evaluate_subrun(
|
|||||||
if latest_artifact_note not in notes and all(result.tests == 0 for result in host_results.values()):
|
if latest_artifact_note not in notes and all(result.tests == 0 for result in host_results.values()):
|
||||||
notes.append(latest_artifact_note)
|
notes.append(latest_artifact_note)
|
||||||
state = "FAILED" if any(result.failures for result in host_results.values()) else "COMPLETED"
|
state = "FAILED" if any(result.failures for result in host_results.values()) else "COMPLETED"
|
||||||
return state, host_results, start_ts, end_ts, subrun.currents_url, notes
|
return state, host_results, start_ts, end_ts, currents_url, notes
|
||||||
|
|
||||||
notes.append("Run process exited before host results were detected.")
|
notes.append("Run process exited before host results were detected.")
|
||||||
return "TERMINATED", host_results, start_ts, end_ts, subrun.currents_url, notes
|
return "TERMINATED", host_results, start_ts, end_ts, currents_url, notes
|
||||||
|
|
||||||
|
|
||||||
def discover_categorized_subruns(
|
def discover_categorized_subruns(
|
||||||
build_name: str,
|
build_name: str,
|
||||||
|
build_dir: Path,
|
||||||
reporter_root: Path,
|
reporter_root: Path,
|
||||||
inventory: Dict[str, str],
|
inventory: Dict[str, str],
|
||||||
log_text: str,
|
log_text: str,
|
||||||
@@ -1030,7 +1066,7 @@ def discover_categorized_subruns(
|
|||||||
host_results=host_results,
|
host_results=host_results,
|
||||||
start_ts=start_ts,
|
start_ts=start_ts,
|
||||||
end_ts=end_ts,
|
end_ts=end_ts,
|
||||||
currents_url=summary["currents_url"] if summary else None,
|
currents_url=(summary["currents_url"] if summary else None) or persisted_currents_url_for_build(build_dir, raw_display_name),
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1059,7 +1095,7 @@ def discover_categorized_subruns(
|
|||||||
host_results=host_results,
|
host_results=host_results,
|
||||||
start_ts=started_at,
|
start_ts=started_at,
|
||||||
end_ts=None,
|
end_ts=None,
|
||||||
currents_url=None,
|
currents_url=persisted_currents_url_for_build(build_dir, current_subrun_build),
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1088,6 +1124,7 @@ def determine_state(
|
|||||||
if metadata.get("categorized"):
|
if metadata.get("categorized"):
|
||||||
subrun_states = discover_categorized_subruns(
|
subrun_states = discover_categorized_subruns(
|
||||||
build_name=build_name,
|
build_name=build_name,
|
||||||
|
build_dir=build_dir,
|
||||||
reporter_root=reporter_root,
|
reporter_root=reporter_root,
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
log_text=log_text,
|
log_text=log_text,
|
||||||
@@ -1106,6 +1143,7 @@ def determine_state(
|
|||||||
subrun=subrun,
|
subrun=subrun,
|
||||||
reporter_root=reporter_root,
|
reporter_root=reporter_root,
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
|
build_dir=build_dir,
|
||||||
end_boundary=next_started_at,
|
end_boundary=next_started_at,
|
||||||
parent_active=active,
|
parent_active=active,
|
||||||
cancelled=cancelled,
|
cancelled=cancelled,
|
||||||
@@ -1150,7 +1188,7 @@ def determine_state(
|
|||||||
parent_end_candidates = [subrun["end_ts"] for subrun in subrun_states if subrun["end_ts"]]
|
parent_end_candidates = [subrun["end_ts"] for subrun in subrun_states if subrun["end_ts"]]
|
||||||
start_ts = min(parent_start_candidates) if parent_start_candidates else started_at
|
start_ts = min(parent_start_candidates) if parent_start_candidates else started_at
|
||||||
end_ts = max(parent_end_candidates) if parent_end_candidates else find_check_xml_end(reporter_root, started_at)
|
end_ts = max(parent_end_candidates) if parent_end_candidates else find_check_xml_end(reporter_root, started_at)
|
||||||
currents_url = extract_currents_url(log_text)
|
currents_url = extract_currents_url(log_text) or latest_currents_url(build_dir)
|
||||||
|
|
||||||
if cancelled:
|
if cancelled:
|
||||||
notes.append("Cancellation marker detected.")
|
notes.append("Cancellation marker detected.")
|
||||||
|
|||||||
Reference in New Issue
Block a user