chore(ci): add daily Registry metrics snapshot workflow

Adds a workflow_dispatch + daily-cron GitHub Action that GETs
https://api.comfy.org/nodes/imagemetahub-comfyui-save and appends an
append-only JSONL record (timestamp, downloads, github_stars, rating,
search_ranking, latest_version, status, plus delta vs the previous
record) to metrics/registry-history.jsonl, then commits it back.

- No external service, no secret, no paid dependency (stdlib urllib + git).
- permissions: contents: write only.
- Bot commit message includes [skip ci] so it cannot trigger other
  push-triggered workflows (e.g. the registry publish workflow already
  scoped to pyproject.toml changes only).
- Does not touch node runtime behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
LuqP2
2026-08-07 20:37:13 -03:00
co-authored by Claude Sonnet 5
parent 814b79a011
commit 3ce2582e3e
2 changed files with 221 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
name: Registry Metrics Snapshot
# Daily snapshot of this node's public ComfyUI Registry metrics
# (https://api.comfy.org/nodes/imagemetahub-comfyui-save), appended
# append-only to metrics/registry-history.jsonl and committed back to main.
#
# No external service, no paid dependency, no secret required.
on:
schedule:
# 06:17 UTC daily. Off-the-hour minute to avoid the top-of-hour scheduler crunch.
- cron: "17 6 * * *"
workflow_dispatch: {}
# Minimal token permissions: only what's needed to commit the snapshot back.
permissions:
contents: write
# Avoid overlapping snapshot commits if a manual run and the cron overlap.
concurrency:
group: registry-metrics-snapshot
cancel-in-progress: false
jobs:
snapshot:
name: Fetch and commit registry snapshot
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'LuqP2' }}
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Fetch registry metrics and append snapshot
run: python scripts/registry_metrics_snapshot.py
- name: Commit snapshot
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git diff --quiet -- metrics/registry-history.jsonl; then
echo "No new snapshot data to commit."
exit 0
fi
git add metrics/registry-history.jsonl
# [skip ci] prevents this bot commit from triggering other push-based
# workflows (e.g. the registry publish workflow) in this repo.
git commit -m "chore(metrics): registry snapshot $(date -u +%Y-%m-%d) [skip ci]"
# Guard against a rare race with another push to main landing
# between checkout and push.
for attempt in 1 2 3; do
if git push origin HEAD:main; then
exit 0
fi
echo "Push rejected (attempt $attempt), rebasing onto latest main..."
git fetch origin main
git rebase origin/main
done
echo "::error::Failed to push registry snapshot after 3 attempts."
exit 1
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Registry Metrics Snapshot
Fetches the current public metrics for this node from the ComfyUI Registry
API and appends one JSON-Lines record to metrics/registry-history.jsonl.
- Append-only: existing lines are never rewritten.
- Stdlib only (urllib), no extra dependencies, no secrets required.
- Safe to run locally: `python scripts/registry_metrics_snapshot.py`
Exit codes:
0 success (record appended, or nothing to do)
1 the registry request failed / returned unexpected data
"""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
NODE_ID = "imagemetahub-comfyui-save"
REGISTRY_URL = f"https://api.comfy.org/nodes/{NODE_ID}"
HISTORY_FILE = Path(__file__).resolve().parent.parent / "metrics" / "registry-history.jsonl"
REQUEST_TIMEOUT = 30
def fetch_registry_data(url: str) -> dict:
request = urllib.request.Request(url, headers={"Accept": "application/json"})
try:
with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response:
body = response.read()
except (urllib.error.URLError, TimeoutError) as error:
raise RuntimeError(f"Failed to reach {url}: {error}") from error
try:
data = json.loads(body)
except json.JSONDecodeError as error:
raise RuntimeError(f"Registry response was not valid JSON: {error}") from error
if not isinstance(data, dict) or "id" not in data:
raise RuntimeError(f"Unexpected registry payload shape: {data!r}")
return data
def read_last_record(path: Path) -> dict | None:
if not path.exists():
return None
last_line = None
with path.open("r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line:
last_line = line
if last_line is None:
return None
try:
return json.loads(last_line)
except json.JSONDecodeError:
return None
def compute_delta(current: dict, previous: dict | None) -> dict | None:
if previous is None:
return None
def numeric_delta(key: str):
cur_val = current.get(key)
prev_val = previous.get(key)
if not isinstance(cur_val, (int, float)) or not isinstance(prev_val, (int, float)):
return None
delta = cur_val - prev_val
return round(delta, 4) if isinstance(delta, float) else delta
return {
"downloads": numeric_delta("downloads"),
"github_stars": numeric_delta("github_stars"),
"rating": numeric_delta("rating"),
"search_ranking": numeric_delta("search_ranking"),
"version_changed": current.get("latest_version") != previous.get("latest_version"),
"status_changed": current.get("status") != previous.get("status"),
}
def build_record(payload: dict, previous: dict | None) -> dict:
latest_version = payload.get("latest_version") or {}
record = {
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"node_id": payload.get("id", NODE_ID),
"downloads": payload.get("downloads"),
"github_stars": payload.get("github_stars"),
"rating": payload.get("rating"),
"search_ranking": payload.get("search_ranking"),
"latest_version": latest_version.get("version"),
"status": payload.get("status"),
}
record["delta"] = compute_delta(record, previous)
return record
def append_record(path: Path, record: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8", newline="\n") as handle:
handle.write(json.dumps(record, ensure_ascii=False, sort_keys=True))
handle.write("\n")
def write_step_summary(record: dict) -> None:
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary_path:
return
delta = record.get("delta") or {}
lines = [
"### Registry metrics snapshot",
"",
f"- timestamp: `{record['timestamp']}`",
f"- downloads: **{record['downloads']}** (Δ {delta.get('downloads', 'n/a')})",
f"- github_stars: **{record['github_stars']}** (Δ {delta.get('github_stars', 'n/a')})",
f"- rating: **{record['rating']}** (Δ {delta.get('rating', 'n/a')})",
f"- search_ranking: **{record['search_ranking']}** (Δ {delta.get('search_ranking', 'n/a')})",
f"- latest_version: **{record['latest_version']}**"
+ (" (changed)" if delta.get("version_changed") else ""),
f"- status: **{record['status']}**"
+ (" (changed)" if delta.get("status_changed") else ""),
]
with open(summary_path, "a", encoding="utf-8") as handle:
handle.write("\n".join(lines) + "\n")
def main() -> int:
try:
payload = fetch_registry_data(REGISTRY_URL)
except RuntimeError as error:
print(f"::error::{error}", file=sys.stderr)
return 1
previous = read_last_record(HISTORY_FILE)
record = build_record(payload, previous)
append_record(HISTORY_FILE, record)
write_step_summary(record)
print(f"Appended snapshot to {HISTORY_FILE}: {json.dumps(record, sort_keys=True)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())