feat(metrics): publish registry summary (#275)

This commit is contained in:
malkuthro
2026-08-06 20:28:04 +02:00
committed by GitHub
parent 2babbafb6b
commit 6ab4476169
4 changed files with 113 additions and 2 deletions
+1 -1
View File
@@ -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
+4
View File
@@ -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
@@ -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
+41 -1
View File
@@ -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} "