From 6ab44761697b1fed489abe80ef93aecc9cc854a5 Mon Sep 17 00:00:00 2001 From: malkuthro <166633396+malkuthro@users.noreply.github.com> Date: Thu, 6 Aug 2026 20:28:04 +0200 Subject: [PATCH] feat(metrics): publish registry summary (#275) --- .github/workflows/registry-metrics.yml | 2 +- CHANGELOG.md | 4 ++ .../scripts/test_collect_registry_metrics.py | 67 +++++++++++++++++++ tools/collect_registry_metrics.py | 42 +++++++++++- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 tests/scripts/test_collect_registry_metrics.py diff --git a/.github/workflows/registry-metrics.yml b/.github/workflows/registry-metrics.yml index 75c8e0c..c46b752 100644 --- a/.github/workflows/registry-metrics.yml +++ b/.github/workflows/registry-metrics.yml @@ -43,7 +43,7 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add metrics/comfy-registry-downloads.csv metrics/README.md + git add metrics/comfy-registry-downloads.csv metrics/README.md metrics/summary.json if git diff --cached --quiet; then echo "No metrics changes to commit." exit 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b929c..a8ead52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format is inspired by Keep a Changelog and SemVer. commit SHA, and enabled one grouped weekly Dependabot update for those pins. ### Added +- **Registry metrics summary.** The daily Comfy Registry collector now writes + `metrics/summary.json` beside its CSV history, giving downstream consumers a + small stable contract for the v0.3.0 downloads, daily-average, peak-day, and + latest-version summary. - **Independent output type for published setups.** A `Koolook_PublishRouter` is auto-detected as the setup's **output selector**: whichever writer branches you wire become the app's "Output type" options, and the external user can pick diff --git a/tests/scripts/test_collect_registry_metrics.py b/tests/scripts/test_collect_registry_metrics.py new file mode 100644 index 0000000..221af5b --- /dev/null +++ b/tests/scripts/test_collect_registry_metrics.py @@ -0,0 +1,67 @@ +"""Contract tests for the public Registry metrics summary.""" +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT = REPO_ROOT / "tools" / "collect_registry_metrics.py" +SPEC = importlib.util.spec_from_file_location("collect_registry_metrics", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +collector = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = collector +SPEC.loader.exec_module(collector) + + +def _row(date: str, downloads: int, delta: int, version: str = "0.4.4") -> dict[str, str]: + return { + "date": date, + "timestamp_utc": f"{date}T08:00:00Z", + "downloads_total": str(downloads), + "daily_delta": str(delta), + "rolling_7d_avg": "0.0", + "latest_version": version, + "latest_status": "Active", + "pending_versions": "", + "active_versions": version, + } + + +def test_summary_matches_the_v030_landing_page_contract(): + rows = [ + _row("2026-08-04", 25315, 15), + _row("2026-08-05", 26242, 927), + _row("2026-08-06", 26870, 628), + ] + + assert collector._make_summary(rows) == { + "asOf": "2026-08-06", + "sinceVersion": "0.3.0", + "downloadsSince": 26775, + "days": 90, + "avgPerDay": 297.5, + "peakDate": "2026-08-05", + "peakDelta": 927, + "latestVersion": "0.4.4", + } + + +def test_summary_is_pretty_printed_json(tmp_path: Path): + summary = collector._make_summary([_row("2026-08-06", 26870, 628)]) + output = tmp_path / "summary.json" + + collector._write_summary(output, summary) + + assert json.loads(output.read_text(encoding="utf-8")) == summary + assert output.read_text(encoding="utf-8").endswith("\n") + + +def test_registry_metrics_workflow_commits_the_summary(): + workflow = (REPO_ROOT / ".github" / "workflows" / "registry-metrics.yml").read_text( + encoding="utf-8" + ) + + assert "metrics/summary.json" in workflow diff --git a/tools/collect_registry_metrics.py b/tools/collect_registry_metrics.py index 76d9e0f..5b1b4b8 100644 --- a/tools/collect_registry_metrics.py +++ b/tools/collect_registry_metrics.py @@ -15,7 +15,7 @@ import sys import urllib.request from collections.abc import Iterable from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import date, datetime, timezone from pathlib import Path from typing import Any @@ -33,6 +33,9 @@ CSV_FIELDS = [ "pending_versions", "active_versions", ] +SUMMARY_BASELINE_DATE = "2026-05-08" +SUMMARY_BASELINE_DOWNLOADS = 95 +SUMMARY_SINCE_VERSION = "0.3.0" @dataclass(frozen=True) @@ -141,6 +144,41 @@ def _upsert_today(rows: list[dict[str, str]], snapshot: Snapshot) -> list[dict[s return [*kept, snapshot.as_row()] +def _make_summary(rows: list[dict[str, str]]) -> dict[str, str | int | float]: + """Build the stable, machine-readable landing-page summary. + + The Registry did not expose per-version downloads at the v0.3.0 release, + so its maintainer-recorded 95-download total on 2026-05-08 is the fixed + baseline for the public "since v0.3.0" metric. + """ + if not rows: + raise ValueError("cannot summarize an empty metrics history") + + latest = rows[-1] + latest_date = date.fromisoformat(latest["date"]) + baseline_date = date.fromisoformat(SUMMARY_BASELINE_DATE) + days = (latest_date - baseline_date).days + if days <= 0: + raise ValueError("latest metrics snapshot must be after the summary baseline") + + downloads_since = int(latest["downloads_total"]) - SUMMARY_BASELINE_DOWNLOADS + peak = max(rows, key=lambda row: int(row.get("daily_delta") or 0)) + return { + "asOf": latest["date"], + "sinceVersion": SUMMARY_SINCE_VERSION, + "downloadsSince": downloads_since, + "days": days, + "avgPerDay": round(downloads_since / days, 1), + "peakDate": peak["date"], + "peakDelta": int(peak.get("daily_delta") or 0), + "latestVersion": latest["latest_version"], + } + + +def _write_summary(path: Path, summary: dict[str, str | int | float]) -> None: + path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + + def _render_overview(node_id: str, rows: list[dict[str, str]]) -> str: if not rows: return f"# Comfy Registry Metrics: {node_id}\n\nNo snapshots recorded yet.\n" @@ -184,6 +222,7 @@ def main() -> int: output_dir = Path(args.output_dir) csv_path = output_dir / "comfy-registry-downloads.csv" overview_path = output_dir / "README.md" + summary_path = output_dir / "summary.json" node = _fetch_json(f"{API_ROOT}/nodes/{args.node}") versions = _fetch_json(f"{API_ROOT}/nodes/{args.node}/versions") @@ -193,6 +232,7 @@ def main() -> int: _write_rows(csv_path, updated_rows) overview_path.write_text(_render_overview(args.node, updated_rows), encoding="utf-8") + _write_summary(summary_path, _make_summary(updated_rows)) print( f"{snapshot.date}: downloads={snapshot.downloads_total} "