ci(metrics): enforce strict traffic API checks across workflows

This commit is contained in:
h3p 2026-03-11 22:19:42 +01:00
parent b7222d507d
commit ffc5b6474b
6 changed files with 51 additions and 64 deletions

View file

@ -1,4 +1,4 @@
name: Update Download Metrics
name: Update Download & Traffic Metrics
on:
release:
@ -25,11 +25,11 @@ jobs:
with:
python-version: "3.12"
- name: Update README metrics and chart
- name: Update README metrics and traffic chart
env:
GH_TOKEN: ${{ secrets.METRIC_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
run: scripts/update_download_metrics.py --require-clone-api
run: scripts/update_download_metrics.py --require-traffic-api
- name: Commit and push if changed
run: |

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 497;
CURRENT_PROJECT_VERSION = 498;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -444,7 +444,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 497;
CURRENT_PROJECT_VERSION = 498;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -151,14 +151,13 @@
<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>
<p align="center">
<img alt="Git clones (14d)" src="https://img.shields.io/static/v1?label=Git+clones+%2814d%29&message=2624&color=7C3AED&style=for-the-badge">
<img alt="GitHub views (14d)" src="https://img.shields.io/static/v1?label=GitHub+views+%2814d%29&message=865&color=0EA5E9&style=for-the-badge">
<img alt="Unique cloners (14d)" src="https://img.shields.io/static/v1?label=Unique+cloners+%2814d%29&message=413&color=7C3AED&style=for-the-badge">
<img alt="Unique visitors (14d)" src="https://img.shields.io/static/v1?label=Unique+visitors+%2814d%29&message=159&color=0EA5E9&style=for-the-badge">
</p>
<p align="center">
<img alt="Clone snapshot (UTC)" src="https://img.shields.io/static/v1?label=Clone+snapshot+%28UTC%29&message=2026-03-09+00%3A00&color=334155&style=flat-square">
<img alt="View snapshot (UTC)" src="https://img.shields.io/static/v1?label=View+snapshot+%28UTC%29&message=2026-03-09+00%3A00&color=334155&style=flat-square">
<img alt="Clone snapshot (UTC)" src="https://img.shields.io/static/v1?label=Clone+snapshot+%28UTC%29&message=2026-03-11+21%3A18&color=334155&style=flat-square">
<img alt="View snapshot (UTC)" src="https://img.shields.io/static/v1?label=View+snapshot+%28UTC%29&message=2026-03-11+21%3A18&color=334155&style=flat-square">
</p>
## Project Docs
- Release history: [`CHANGELOG.md`](CHANGELOG.md)

View file

@ -83,17 +83,17 @@
<text x="776" y="56" fill="#D7F7FF" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Release trend line with highlighted points</text>
<rect x="58" y="378" width="1084" height="210" rx="12" fill="#0A1A2B" stroke="#2A4762" stroke-width="1"/>
<text x="84" y="412" fill="#E6F3FF" font-size="20" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="700">Repository Traffic (last 14 days)</text>
<text x="86" y="438" fill="#C4B5FD" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Clones: 2624</text>
<text x="86" y="438" fill="#C4B5FD" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Unique cloners: 413</text>
<rect x="86" y="450" width="1024" height="26" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>
<rect x="86" y="450" width="959.6" height="26" rx="10" fill="url(#cloneFill)"/>
<text x="86" y="498" fill="#7DD3FC" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Views: 865</text>
<rect x="86" y="450" width="528.6" height="26" rx="10" fill="url(#cloneFill)"/>
<text x="86" y="498" fill="#7DD3FC" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Unique visitors: 159</text>
<rect x="86" y="510" width="1024" height="26" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>
<rect x="86" y="510" width="316.3" height="26" rx="10" fill="url(#viewFill)"/>
<rect x="86" y="510" width="203.5" height="26" rx="10" fill="url(#viewFill)"/>
<line x1="86" y1="430" x2="86" y2="548" stroke="#436280" stroke-width="1"/>
<line x1="598.0" y1="430" x2="598.0" y2="548" stroke="#436280" stroke-width="1"/>
<line x1="1110" y1="430" x2="1110" y2="548" stroke="#436280" stroke-width="1"/>
<text x="84" y="566" text-anchor="start" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">0</text>
<text x="598.0" y="566" text-anchor="middle" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">1400</text>
<text x="1112" y="566" text-anchor="end" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">2800</text>
<text x="86" y="588" fill="#9CC3E6" font-size="14" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Shared scale: 0 to 2800 events in the last 14 days.</text>
<text x="598.0" y="566" text-anchor="middle" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">400</text>
<text x="1112" y="566" text-anchor="end" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">800</text>
<text x="86" y="588" fill="#9CC3E6" font-size="14" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Shared scale: 0 to 800 events in the last 14 days.</text>
</svg>

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -29,13 +29,13 @@ grep -nE "^> Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
grep -nE "^- Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null
grep -nE "^\\| .*\\(https://github\\.com/h3pdesign/Neon-Vision-Editor/releases/tag/${TAG}\\) \\|" README.md >/dev/null
echo "Validating README download metrics freshness..."
echo "Validating README download + traffic metrics freshness..."
if gh release view "$TAG" >/dev/null 2>&1; then
is_draft="$(gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null || echo "false")"
if [[ "$is_draft" == "true" ]]; then
echo "Skipping metrics freshness check: ${TAG} exists as a draft release."
else
scripts/update_download_metrics.py --check
scripts/update_download_metrics.py --check --require-traffic-api
fi
else
echo "Skipping metrics freshness check: ${TAG} is not published on GitHub releases yet."

View file

@ -154,8 +154,11 @@ def fetch_clone_traffic() -> tuple[list[ClonePoint], int | None, dt.datetime | N
points.append(ClonePoint(timestamp=ts, count=count))
points.sort(key=lambda p: p.timestamp)
unique_total = payload.get("uniques")
total_count = payload.get("count")
latest_timestamp = points[-1].timestamp if points else None
if isinstance(unique_total, int):
return points, unique_total, latest_timestamp
if isinstance(total_count, int):
return points, total_count, latest_timestamp
return points, None, latest_timestamp
@ -201,8 +204,11 @@ def fetch_view_traffic() -> tuple[list[ViewPoint], int | None, dt.datetime | Non
points.append(ViewPoint(timestamp=ts, count=count))
points.sort(key=lambda p: p.timestamp)
unique_total = payload.get("uniques")
total_count = payload.get("count")
latest_timestamp = points[-1].timestamp if points else None
if isinstance(unique_total, int):
return points, unique_total, latest_timestamp
if isinstance(total_count, int):
return points, total_count, latest_timestamp
return points, None, latest_timestamp
@ -299,10 +305,10 @@ def generate_svg(points: list[ReleasePoint], clone_total: int, view_total: int,
mid_x = panel_left + (track_width * 0.5)
clone_panel.extend(
[
f' <text x="{panel_left}" y="{clone_bar_top - 12}" fill="#C4B5FD" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Clones: {clone_total}</text>',
f' <text x="{panel_left}" y="{clone_bar_top - 12}" fill="#C4B5FD" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Unique cloners: {clone_total}</text>',
f' <rect x="{panel_left}" y="{clone_bar_top}" width="{track_width}" height="{clone_bar_bottom - clone_bar_top}" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>',
f' <rect x="{panel_left}" y="{clone_bar_top}" width="{clone_fill_width:.1f}" height="{clone_bar_bottom - clone_bar_top}" rx="10" fill="url(#cloneFill)"/>',
f' <text x="{panel_left}" y="{view_bar_top - 12}" fill="#7DD3FC" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Views: {view_total}</text>',
f' <text x="{panel_left}" y="{view_bar_top - 12}" fill="#7DD3FC" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Unique visitors: {view_total}</text>',
f' <rect x="{panel_left}" y="{view_bar_top}" width="{track_width}" height="{view_bar_bottom - view_bar_top}" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>',
f' <rect x="{panel_left}" y="{view_bar_top}" width="{view_fill_width:.1f}" height="{view_bar_bottom - view_bar_top}" rx="10" fill="url(#viewFill)"/>',
f' <line x1="{panel_left}" y1="{clone_bar_top - 20}" x2="{panel_left}" y2="{view_bar_bottom + 12}" stroke="#436280" stroke-width="1"/>',
@ -398,14 +404,6 @@ def parse_existing_clone_total(content: str) -> int | None:
return None
def parse_existing_clone_snapshot(content: str) -> str | None:
match = re.search(r"Clone data snapshot \(UTC\): <strong>([^<]+)</strong>\.", content)
if not match:
return None
value = match.group(1).strip()
return value if value else None
def parse_existing_view_total(content: str) -> int | None:
match = re.search(r"GitHub views \(last \d+ days\): <strong>(\d+)</strong>\.", content)
if not match:
@ -416,14 +414,6 @@ def parse_existing_view_total(content: str) -> int | None:
return None
def parse_existing_view_snapshot(content: str) -> str | None:
match = re.search(r"View data snapshot \(UTC\): <strong>([^<]+)</strong>\.", content)
if not match:
return None
value = match.group(1).strip()
return value if value else None
def shields_badge(label: str, message: str, color: str, style: str = "for-the-badge") -> str:
query = urllib.parse.urlencode(
{
@ -511,29 +501,28 @@ def update_readme(
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
content,
)
content = re.sub(
r'(?s)<p align="center">\s*<img alt="(?:Git clones|Unique cloners) \(14d\)".*?</p>\s*'
r'<p align="center">\s*<img alt="Clone snapshot \(UTC\)".*?</p>\s*',
"",
content,
)
traffic_badges = (
"<p align=\"center\">\n"
f" <img alt=\"Git clones (14d)\" src=\"{shields_badge('Git clones (14d)', str(clone_total), '7C3AED')}\">\n"
f" <img alt=\"GitHub views (14d)\" src=\"{shields_badge('GitHub views (14d)', str(view_total), '0EA5E9')}\">\n"
f" <img alt=\"Unique cloners (14d)\" src=\"{shields_badge('Unique cloners (14d)', str(clone_total), '7C3AED')}\">\n"
f" <img alt=\"Unique visitors (14d)\" src=\"{shields_badge('Unique visitors (14d)', str(view_total), '0EA5E9')}\">\n"
"</p>\n"
"<p align=\"center\">\n"
f" <img alt=\"Clone snapshot (UTC)\" src=\"{shields_badge('Clone snapshot (UTC)', clone_snapshot_utc, '334155', style='flat-square')}\">\n"
f" <img alt=\"View snapshot (UTC)\" src=\"{shields_badge('View snapshot (UTC)', view_snapshot_utc, '334155', style='flat-square')}\">\n"
"</p>"
)
old_badges_pattern = (
r'(?s)<p align="center">\s*<img alt="Git clones \(14d\)".*?</p>\s*'
r'<p align="center">\s*<img alt="Clone snapshot \(UTC\)".*?</p>'
content = content.replace(
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>\n'
+ traffic_badges,
1,
)
if re.search(old_badges_pattern, content):
content = re.sub(old_badges_pattern, traffic_badges, content)
else:
content = content.replace(
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>\n'
+ traffic_badges,
1,
)
content = re.sub(
r"(?m)^> Latest release: \*\*.*\*\*$",
f"> Latest release: **{latest_tag}**",
@ -562,16 +551,23 @@ def total_downloads_for_scale(points: list[ReleasePoint]) -> int:
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Refresh README download metrics and chart.")
parser.add_argument("--check", action="store_true", help="Fail if files are not up-to-date.")
parser.add_argument(
"--require-traffic-api",
action="store_true",
help="Fail if clone/view traffic API data cannot be fetched.",
)
parser.add_argument(
"--require-clone-api",
action="store_true",
help="Fail if clone traffic API data cannot be fetched.",
help=argparse.SUPPRESS,
)
return parser.parse_args()
def main() -> int:
args = parse_args()
require_traffic_api = args.require_traffic_api or args.require_clone_api
update_timestamp_utc = dt.datetime.now(dt.UTC).strftime("%Y-%m-%d %H:%M")
releases = fetch_releases()
clone_points, clone_total_api, clone_latest_timestamp = fetch_clone_traffic()
view_points, view_total_api, view_latest_timestamp = fetch_view_traffic()
@ -584,16 +580,14 @@ def main() -> int:
readme_before = README.read_text(encoding="utf-8")
existing_clone_total = parse_existing_clone_total(readme_before)
existing_clone_snapshot = parse_existing_clone_snapshot(readme_before)
existing_view_total = parse_existing_view_total(readme_before)
existing_view_snapshot = parse_existing_view_snapshot(readme_before)
if args.require_clone_api and (clone_total_api is None or clone_latest_timestamp is None):
if require_traffic_api and (clone_total_api is None or clone_latest_timestamp is None):
print(
"Clone traffic API unavailable. Refusing to reuse stale README clone metrics.",
file=sys.stderr,
)
return 1
if args.require_clone_api and (view_total_api is None or view_latest_timestamp is None):
if require_traffic_api and (view_total_api is None or view_latest_timestamp is None):
print(
"View traffic API unavailable. Refusing to reuse stale README view metrics.",
file=sys.stderr,
@ -605,20 +599,14 @@ def main() -> int:
file=sys.stderr,
)
clone_total = clone_total_api if clone_total_api is not None else (existing_clone_total or 0)
if clone_latest_timestamp is not None:
clone_snapshot_utc = clone_latest_timestamp.astimezone(dt.UTC).strftime("%Y-%m-%d %H:%M")
else:
clone_snapshot_utc = existing_clone_snapshot or f"{snapshot_date} 00:00"
clone_snapshot_utc = update_timestamp_utc
if view_total_api is None:
print(
"Warning: view traffic API unavailable; reusing existing README view total.",
file=sys.stderr,
)
view_total = view_total_api if view_total_api is not None else (existing_view_total or 0)
if view_latest_timestamp is not None:
view_snapshot_utc = view_latest_timestamp.astimezone(dt.UTC).strftime("%Y-%m-%d %H:%M")
else:
view_snapshot_utc = existing_view_snapshot or f"{snapshot_date} 00:00"
view_snapshot_utc = update_timestamp_utc
svg = generate_svg(trend_points, clone_total, view_total, snapshot_date)
readme_after = update_readme(
readme_before,