174 lines
6.1 KiB
Python
174 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate badge SVG files and push them to the ``badges`` branch.
|
|
|
|
Also updates README.md and docs/index.md on master with cache-busting
|
|
``raw/commit/<sha>/badge.svg`` URLs so that browsers always fetch the
|
|
latest badge version (Gitea caches ``raw/branch/`` URLs for 6 hours).
|
|
|
|
The script fetches the latest master before generating badges so that
|
|
the version badge always reflects the current state of the repository
|
|
(even if a release commit was pushed moments before by the parallel
|
|
release job).
|
|
|
|
Usage::
|
|
|
|
python3 scripts/ci/push_badges.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess # nosec B404
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import click
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
|
|
# Badge filenames that get pushed to the badges branch
|
|
BADGE_FILES = ["coverage.svg", "tests.svg", "docs.svg", "quality.svg", "version.svg", "python.svg"]
|
|
|
|
# Files that contain badge URLs and need to be updated
|
|
FILES_WITH_BADGE_URLS = ["README.md", "docs/index.md"]
|
|
|
|
# Pattern to match raw/branch/badges/<name>.svg URLs
|
|
_BADGE_URL_RE = re.compile(r"(https://[^/]+/[^/]+/[^/]+/raw/)(?:branch/badges|commit/[0-9a-f]{40})/([a-z_]+\.svg)")
|
|
|
|
|
|
def _run(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
"""Run a command and return the result."""
|
|
return subprocess.run(cmd, check=True, text=True, **kwargs) # nosec B603
|
|
|
|
|
|
def _run_capture(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
"""Run a command and capture stdout."""
|
|
return subprocess.run(cmd, check=True, text=True, capture_output=True, **kwargs) # nosec B603
|
|
|
|
|
|
def fetch_latest_master(branch: str = "master") -> None:
|
|
"""Fetch and hard-reset to the latest remote branch.
|
|
|
|
Ensures the working tree reflects the absolute latest state of the
|
|
remote, which is critical when the release job may have just pushed
|
|
a new version commit.
|
|
"""
|
|
_run(["git", "fetch", "origin", branch]) # nosec B607
|
|
_run(["git", "reset", "--hard", f"origin/{branch}"]) # nosec B607
|
|
click.echo(f"Synced to latest origin/{branch}")
|
|
|
|
|
|
def generate_badges(output_dir: str) -> None:
|
|
"""Generate badge SVG files using generate_badges.py."""
|
|
_run([sys.executable, "scripts/generate_badges.py", "--output-dir", output_dir])
|
|
badges = list(Path(output_dir).glob("*.svg"))
|
|
if not badges:
|
|
raise click.ClickException("No badge SVG files generated")
|
|
click.echo(f"Generated {len(badges)} badge files")
|
|
|
|
|
|
def push_to_badges_branch(badges_dir: str) -> str:
|
|
"""Push generated badges to the orphan ``badges`` branch.
|
|
|
|
Returns the commit SHA of the pushed badges branch.
|
|
"""
|
|
_run(["git", "config", "user.name", "gitea-actions-bot"]) # nosec B607
|
|
_run(["git", "config", "user.email", "actions@oblachno.fyi"]) # nosec B607
|
|
_run(["git", "checkout", "--orphan", "badges"]) # nosec B607
|
|
_run(["git", "rm", "-rf", "."]) # nosec B607
|
|
|
|
# Copy badge files to root
|
|
import shutil
|
|
|
|
for svg in Path(badges_dir).glob("*.svg"):
|
|
shutil.copy2(svg, Path.cwd() / svg.name)
|
|
|
|
_run(["git", "add", "./*.svg"]) # nosec B607
|
|
_run(["git", "commit", "--no-verify", "-m", "Update badges [skip ci]"]) # nosec B607
|
|
_run(["git", "push", "origin", "badges", "--force"]) # nosec B607
|
|
click.echo("Badges pushed to badges branch")
|
|
|
|
# Get the commit SHA of the badges branch
|
|
result = _run_capture(["git", "rev-parse", "HEAD"]) # nosec B607
|
|
sha = result.stdout.strip()
|
|
click.echo(f"Badges commit SHA: {sha}")
|
|
return sha
|
|
|
|
|
|
def update_badge_urls(content: str, badges_sha: str) -> str:
|
|
"""Replace raw/branch/badges/<name>.svg URLs with raw/commit/<sha>/<name>.svg.
|
|
|
|
This bypasses Gitea's 6-hour cache on raw/branch/ URLs by using a
|
|
URL that changes each time the badges branch is updated.
|
|
"""
|
|
return _BADGE_URL_RE.sub(
|
|
lambda m: f"{m.group(1)}commit/{badges_sha}/{m.group(2)}",
|
|
content,
|
|
)
|
|
|
|
|
|
def update_readme_with_badge_sha(badges_sha: str, repo_root: Path | None = None) -> None:
|
|
"""Update README.md and docs/index.md with cache-busting badge URLs.
|
|
|
|
Switches back to master, replaces ``raw/branch/badges/`` URLs with
|
|
``raw/commit/<sha>/`` URLs, commits and pushes.
|
|
"""
|
|
root = repo_root or REPO_ROOT
|
|
|
|
# Switch back to master
|
|
_run(["git", "checkout", "master"]) # nosec B607
|
|
_run(["git", "fetch", "origin", "master"]) # nosec B607
|
|
_run(["git", "reset", "--hard", "origin/master"]) # nosec B607
|
|
|
|
updated_any = False
|
|
for filename in FILES_WITH_BADGE_URLS:
|
|
filepath = root / filename
|
|
if not filepath.exists():
|
|
continue
|
|
content = filepath.read_text()
|
|
new_content = update_badge_urls(content, badges_sha)
|
|
if new_content != content:
|
|
filepath.write_text(new_content)
|
|
click.echo(f"Updated badge URLs in {filename}")
|
|
updated_any = True
|
|
|
|
if not updated_any:
|
|
click.echo("No badge URLs found to update — README already up to date")
|
|
return
|
|
|
|
_run(["git", "add", "README.md", "docs/index.md"]) # nosec B607
|
|
_run(
|
|
[
|
|
"git",
|
|
"commit",
|
|
"--no-verify",
|
|
"-m",
|
|
f"chore: update badge URLs to commit {badges_sha[:8]} [skip ci]",
|
|
]
|
|
) # nosec B607
|
|
_run(["git", "push", "origin", "master"]) # nosec B607
|
|
click.echo(f"Pushed README update with badge SHA {badges_sha[:8]}")
|
|
|
|
|
|
@click.command()
|
|
@click.option("--output-dir", default=".badges/", help="Temporary directory for badge files.")
|
|
@click.option("--branch", default="master", help="Branch to sync before generating badges.")
|
|
@click.option(
|
|
"--no-readme-update",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Skip updating README with cache-busting URLs (for local testing).",
|
|
)
|
|
def main(output_dir: str, branch: str, no_readme_update: bool) -> None:
|
|
"""Generate badges and push them to the badges branch."""
|
|
fetch_latest_master(branch)
|
|
generate_badges(output_dir)
|
|
badges_sha = push_to_badges_branch(output_dir)
|
|
if not no_readme_update:
|
|
update_readme_with_badge_sha(badges_sha)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main() # pragma: no cover
|