|
27 | 27 | from modal_app.step_manifests.specs import RUN_MANIFEST_STEP_IDS, step_title |
28 | 28 |
|
29 | 29 | PIPELINE_STATUS_SCHEMA_VERSION = "1" |
| 30 | +DEFAULT_RUNS_LIMIT = 25 |
| 31 | +MAX_RUNS_LIMIT = 100 |
30 | 32 |
|
31 | 33 |
|
32 | 34 | def _run_dir(run_id: str, runs_dir: str | Path | None = None) -> Path: |
@@ -149,6 +151,176 @@ def _manifest_payload(manifest) -> dict[str, Any]: |
149 | 151 | } |
150 | 152 |
|
151 | 153 |
|
| 154 | +def _bounded_limit(limit: int | str | None) -> int: |
| 155 | + try: |
| 156 | + parsed = int(limit if limit is not None else DEFAULT_RUNS_LIMIT) |
| 157 | + except (TypeError, ValueError): |
| 158 | + parsed = DEFAULT_RUNS_LIMIT |
| 159 | + return max(0, min(parsed, MAX_RUNS_LIMIT)) |
| 160 | + |
| 161 | + |
| 162 | +def _index_error_payload(error: dict[str, Any] | None) -> dict[str, Any] | None: |
| 163 | + if error is None: |
| 164 | + return None |
| 165 | + allowed = ( |
| 166 | + "stage_id", |
| 167 | + "substage_id", |
| 168 | + "surface", |
| 169 | + "error_type", |
| 170 | + "message", |
| 171 | + "message_truncated", |
| 172 | + "record_path", |
| 173 | + "latest_path", |
| 174 | + "traceback_available", |
| 175 | + ) |
| 176 | + return {key: error[key] for key in allowed if key in error} |
| 177 | + |
| 178 | + |
| 179 | +def _latest_manifest_payload( |
| 180 | + stage_manifests: list[dict[str, Any]], |
| 181 | +) -> dict[str, Any] | None: |
| 182 | + if not stage_manifests: |
| 183 | + return None |
| 184 | + item = stage_manifests[-1] |
| 185 | + manifest = item["manifest"] |
| 186 | + return { |
| 187 | + "step_id": item["step_id"], |
| 188 | + "stage_id": item["stage_id"], |
| 189 | + "substage_id": item["substage_id"], |
| 190 | + "title": item["title"], |
| 191 | + "status": item["status"], |
| 192 | + "started_at": manifest.get("started_at"), |
| 193 | + "completed_at": manifest.get("completed_at"), |
| 194 | + "duration_s": manifest.get("duration_s"), |
| 195 | + "reuse_decision": manifest.get("reuse_decision", "not_applicable"), |
| 196 | + } |
| 197 | + |
| 198 | + |
| 199 | +def _run_index_item( |
| 200 | + run_id: str, |
| 201 | + *, |
| 202 | + runs_dir: str | Path | None = None, |
| 203 | +) -> dict[str, Any]: |
| 204 | + payload = build_pipeline_status_payload(run_id, runs_dir=runs_dir) |
| 205 | + run_manifest = payload.get("run_manifest") or {} |
| 206 | + stage_manifests = payload.get("stage_manifests") or [] |
| 207 | + missing = payload.get("missing_expected_manifest_ids") or [] |
| 208 | + expected = list(run_manifest.get("known_step_ids") or RUN_MANIFEST_STEP_IDS) |
| 209 | + return { |
| 210 | + "run_id": payload["run_id"], |
| 211 | + "status": payload["status"], |
| 212 | + "message": payload["message"], |
| 213 | + "branch": run_manifest.get("branch"), |
| 214 | + "sha": run_manifest.get("sha"), |
| 215 | + "candidate_version": run_manifest.get("candidate_version"), |
| 216 | + "release_version": run_manifest.get("release_version"), |
| 217 | + "started_at": run_manifest.get("started_at"), |
| 218 | + "updated_at": payload.get("updated_at"), |
| 219 | + "completed_at": run_manifest.get("completed_at"), |
| 220 | + "modal_app_name": payload.get("modal_app_name"), |
| 221 | + "modal_environment": payload.get("modal_environment"), |
| 222 | + "hf_staging_prefix": run_manifest.get("hf_staging_prefix"), |
| 223 | + "github_run_url": (run_manifest.get("run_context") or {}).get("github_run_url"), |
| 224 | + "latest_manifest": _latest_manifest_payload(stage_manifests), |
| 225 | + "progress": { |
| 226 | + "expected_manifests": len(expected), |
| 227 | + "present_manifests": len(stage_manifests), |
| 228 | + "missing_manifests": len(missing), |
| 229 | + }, |
| 230 | + "error": _index_error_payload(payload.get("error")), |
| 231 | + } |
| 232 | + |
| 233 | + |
| 234 | +def _unreadable_run_index_item(run_id: str, exc: BaseException) -> dict[str, Any]: |
| 235 | + message = redacted_bounded_error_text( |
| 236 | + f"{type(exc).__name__}: {exc}", |
| 237 | + max_chars=DEFAULT_ERROR_MESSAGE_MAX_CHARS, |
| 238 | + ).text |
| 239 | + return { |
| 240 | + "run_id": run_id, |
| 241 | + "status": "unreadable", |
| 242 | + "message": message, |
| 243 | + "branch": None, |
| 244 | + "sha": None, |
| 245 | + "candidate_version": None, |
| 246 | + "release_version": None, |
| 247 | + "started_at": None, |
| 248 | + "updated_at": None, |
| 249 | + "completed_at": None, |
| 250 | + "modal_app_name": None, |
| 251 | + "modal_environment": None, |
| 252 | + "hf_staging_prefix": None, |
| 253 | + "github_run_url": None, |
| 254 | + "latest_manifest": None, |
| 255 | + "progress": { |
| 256 | + "expected_manifests": 0, |
| 257 | + "present_manifests": 0, |
| 258 | + "missing_manifests": 0, |
| 259 | + }, |
| 260 | + "error": { |
| 261 | + "error_type": type(exc).__name__, |
| 262 | + "message": message, |
| 263 | + "traceback_available": False, |
| 264 | + }, |
| 265 | + } |
| 266 | + |
| 267 | + |
| 268 | +def _run_sort_key(item: dict[str, Any]) -> tuple[str, str]: |
| 269 | + return ( |
| 270 | + str(item.get("updated_at") or item.get("started_at") or ""), |
| 271 | + str(item.get("run_id") or ""), |
| 272 | + ) |
| 273 | + |
| 274 | + |
| 275 | +def build_pipeline_runs_payload( |
| 276 | + *, |
| 277 | + limit: int | str | None = DEFAULT_RUNS_LIMIT, |
| 278 | + status: str = "", |
| 279 | + branch: str = "", |
| 280 | + runs_dir: str | Path | None = None, |
| 281 | +) -> dict[str, Any]: |
| 282 | + """Build a JSON-serializable index of recent pipeline runs.""" |
| 283 | + |
| 284 | + bounded_limit = _bounded_limit(limit) |
| 285 | + root = Path(runs_dir) if runs_dir is not None else Path(pipeline_state.RUNS_DIR) |
| 286 | + filters = {"status": status or "", "branch": branch or ""} |
| 287 | + if not root.exists(): |
| 288 | + return { |
| 289 | + "schema_version": PIPELINE_STATUS_SCHEMA_VERSION, |
| 290 | + "count": 0, |
| 291 | + "limit": bounded_limit, |
| 292 | + "filters": filters, |
| 293 | + "runs": [], |
| 294 | + } |
| 295 | + |
| 296 | + items = [] |
| 297 | + for entry in root.iterdir(): |
| 298 | + if not entry.is_dir(): |
| 299 | + continue |
| 300 | + manifest_path = run_manifest_path(entry) |
| 301 | + if not manifest_path.exists(): |
| 302 | + continue |
| 303 | + try: |
| 304 | + item = _run_index_item(entry.name, runs_dir=root) |
| 305 | + except Exception as exc: |
| 306 | + item = _unreadable_run_index_item(entry.name, exc) |
| 307 | + if filters["status"] and item.get("status") != filters["status"]: |
| 308 | + continue |
| 309 | + if filters["branch"] and item.get("branch") != filters["branch"]: |
| 310 | + continue |
| 311 | + items.append(item) |
| 312 | + |
| 313 | + items.sort(key=_run_sort_key, reverse=True) |
| 314 | + runs = items[:bounded_limit] |
| 315 | + return { |
| 316 | + "schema_version": PIPELINE_STATUS_SCHEMA_VERSION, |
| 317 | + "count": len(runs), |
| 318 | + "limit": bounded_limit, |
| 319 | + "filters": filters, |
| 320 | + "runs": runs, |
| 321 | + } |
| 322 | + |
| 323 | + |
152 | 324 | def build_pipeline_status_payload( |
153 | 325 | run_id: str, |
154 | 326 | *, |
|
0 commit comments