diff --git a/.github/data/star-history.json b/.github/data/star-history.json new file mode 100644 index 00000000..f60ee2b5 --- /dev/null +++ b/.github/data/star-history.json @@ -0,0 +1,231 @@ +{ + "repository": "Roboparty/roboto_origin", + "created_at": "2025-12-31", + "snapshots": [ + {"date": "2025-12-31", "stars": 4}, + {"date": "2026-01-01", "stars": 4}, + {"date": "2026-01-02", "stars": 4}, + {"date": "2026-01-03", "stars": 4}, + {"date": "2026-01-04", "stars": 31}, + {"date": "2026-01-05", "stars": 36}, + {"date": "2026-01-06", "stars": 42}, + {"date": "2026-01-07", "stars": 53}, + {"date": "2026-01-08", "stars": 59}, + {"date": "2026-01-09", "stars": 68}, + {"date": "2026-01-10", "stars": 69}, + {"date": "2026-01-11", "stars": 71}, + {"date": "2026-01-12", "stars": 75}, + {"date": "2026-01-13", "stars": 82}, + {"date": "2026-01-14", "stars": 87}, + {"date": "2026-01-15", "stars": 110}, + {"date": "2026-01-16", "stars": 135}, + {"date": "2026-01-17", "stars": 149}, + {"date": "2026-01-18", "stars": 152}, + {"date": "2026-01-19", "stars": 217}, + {"date": "2026-01-20", "stars": 289}, + {"date": "2026-01-21", "stars": 349}, + {"date": "2026-01-22", "stars": 386}, + {"date": "2026-01-23", "stars": 405}, + {"date": "2026-01-24", "stars": 414}, + {"date": "2026-01-25", "stars": 418}, + {"date": "2026-01-26", "stars": 433}, + {"date": "2026-01-27", "stars": 491}, + {"date": "2026-01-28", "stars": 543}, + {"date": "2026-01-29", "stars": 569}, + {"date": "2026-01-30", "stars": 577}, + {"date": "2026-01-31", "stars": 584}, + {"date": "2026-02-01", "stars": 591}, + {"date": "2026-02-02", "stars": 600}, + {"date": "2026-02-03", "stars": 605}, + {"date": "2026-02-04", "stars": 615}, + {"date": "2026-02-05", "stars": 620}, + {"date": "2026-02-06", "stars": 860}, + {"date": "2026-02-07", "stars": 921}, + {"date": "2026-02-08", "stars": 939}, + {"date": "2026-02-09", "stars": 955}, + {"date": "2026-02-10", "stars": 977}, + {"date": "2026-02-11", "stars": 1021}, + {"date": "2026-02-12", "stars": 1040}, + {"date": "2026-02-13", "stars": 1057}, + {"date": "2026-02-14", "stars": 1068}, + {"date": "2026-02-15", "stars": 1077}, + {"date": "2026-02-16", "stars": 1094}, + {"date": "2026-02-17", "stars": 1118}, + {"date": "2026-02-18", "stars": 1160}, + {"date": "2026-02-19", "stars": 1188}, + {"date": "2026-02-20", "stars": 1209}, + {"date": "2026-02-21", "stars": 1212}, + {"date": "2026-02-22", "stars": 1222}, + {"date": "2026-02-23", "stars": 1233}, + {"date": "2026-02-24", "stars": 1253}, + {"date": "2026-02-25", "stars": 1263}, + {"date": "2026-02-26", "stars": 1271}, + {"date": "2026-02-27", "stars": 1280}, + {"date": "2026-02-28", "stars": 1287}, + {"date": "2026-03-01", "stars": 1293}, + {"date": "2026-03-02", "stars": 1302}, + {"date": "2026-03-03", "stars": 1315}, + {"date": "2026-03-04", "stars": 1317}, + {"date": "2026-03-05", "stars": 1324}, + {"date": "2026-03-06", "stars": 1329}, + {"date": "2026-03-07", "stars": 1333}, + {"date": "2026-03-08", "stars": 1336}, + {"date": "2026-03-09", "stars": 1343}, + {"date": "2026-03-10", "stars": 1348}, + {"date": "2026-03-11", "stars": 1352}, + {"date": "2026-03-12", "stars": 1357}, + {"date": "2026-03-13", "stars": 1366}, + {"date": "2026-03-14", "stars": 1369}, + {"date": "2026-03-15", "stars": 1373}, + {"date": "2026-03-16", "stars": 1380}, + {"date": "2026-03-17", "stars": 1387}, + {"date": "2026-03-18", "stars": 1392}, + {"date": "2026-03-19", "stars": 1398}, + {"date": "2026-03-20", "stars": 1400}, + {"date": "2026-03-21", "stars": 1404}, + {"date": "2026-03-22", "stars": 1405}, + {"date": "2026-03-23", "stars": 1409}, + {"date": "2026-03-24", "stars": 1419}, + {"date": "2026-03-25", "stars": 1425}, + {"date": "2026-03-26", "stars": 1430}, + {"date": "2026-03-27", "stars": 1436}, + {"date": "2026-03-28", "stars": 1437}, + {"date": "2026-03-29", "stars": 1437}, + {"date": "2026-03-30", "stars": 1442}, + {"date": "2026-03-31", "stars": 1450}, + {"date": "2026-04-01", "stars": 1462}, + {"date": "2026-04-02", "stars": 1471}, + {"date": "2026-04-03", "stars": 1474}, + {"date": "2026-04-04", "stars": 1475}, + {"date": "2026-04-05", "stars": 1477}, + {"date": "2026-04-06", "stars": 1482}, + {"date": "2026-04-07", "stars": 1488}, + {"date": "2026-04-08", "stars": 1493}, + {"date": "2026-04-09", "stars": 1500}, + {"date": "2026-04-10", "stars": 1503}, + {"date": "2026-04-11", "stars": 1504}, + {"date": "2026-04-12", "stars": 1504}, + {"date": "2026-04-13", "stars": 1507}, + {"date": "2026-04-14", "stars": 1509}, + {"date": "2026-04-15", "stars": 1517}, + {"date": "2026-04-16", "stars": 1520}, + {"date": "2026-04-17", "stars": 1525}, + {"date": "2026-04-18", "stars": 1525}, + {"date": "2026-04-19", "stars": 1527}, + {"date": "2026-04-20", "stars": 1535}, + {"date": "2026-04-21", "stars": 1539}, + {"date": "2026-04-22", "stars": 1544}, + {"date": "2026-04-23", "stars": 1598}, + {"date": "2026-04-24", "stars": 1613}, + {"date": "2026-04-25", "stars": 1618}, + {"date": "2026-04-26", "stars": 1622}, + {"date": "2026-04-27", "stars": 1623}, + {"date": "2026-04-28", "stars": 1628}, + {"date": "2026-04-29", "stars": 1633}, + {"date": "2026-04-30", "stars": 1635}, + {"date": "2026-05-01", "stars": 1635}, + {"date": "2026-05-02", "stars": 1636}, + {"date": "2026-05-03", "stars": 1637}, + {"date": "2026-05-04", "stars": 1637}, + {"date": "2026-05-05", "stars": 1639}, + {"date": "2026-05-06", "stars": 1646}, + {"date": "2026-05-07", "stars": 1652}, + {"date": "2026-05-08", "stars": 1656}, + {"date": "2026-05-09", "stars": 1661}, + {"date": "2026-05-10", "stars": 1663}, + {"date": "2026-05-11", "stars": 1666}, + {"date": "2026-05-12", "stars": 1669}, + {"date": "2026-05-13", "stars": 1673}, + {"date": "2026-05-14", "stars": 1674}, + {"date": "2026-05-15", "stars": 1677}, + {"date": "2026-05-16", "stars": 1678}, + {"date": "2026-05-17", "stars": 1679}, + {"date": "2026-05-18", "stars": 1682}, + {"date": "2026-05-19", "stars": 1694}, + {"date": "2026-05-20", "stars": 1697}, + {"date": "2026-05-21", "stars": 1701}, + {"date": "2026-05-22", "stars": 1703}, + {"date": "2026-05-23", "stars": 1705}, + {"date": "2026-05-24", "stars": 1707}, + {"date": "2026-05-25", "stars": 1713}, + {"date": "2026-05-26", "stars": 1717}, + {"date": "2026-05-27", "stars": 1725}, + {"date": "2026-05-28", "stars": 1733}, + {"date": "2026-05-29", "stars": 1747}, + {"date": "2026-05-30", "stars": 1751}, + {"date": "2026-05-31", "stars": 1769}, + {"date": "2026-06-01", "stars": 1805}, + {"date": "2026-06-02", "stars": 1815}, + {"date": "2026-06-03", "stars": 1834}, + {"date": "2026-06-04", "stars": 1842}, + {"date": "2026-06-05", "stars": 1853}, + {"date": "2026-06-06", "stars": 1856}, + {"date": "2026-06-07", "stars": 1856}, + {"date": "2026-06-08", "stars": 1861}, + {"date": "2026-06-09", "stars": 1881}, + {"date": "2026-06-10", "stars": 1893}, + {"date": "2026-06-11", "stars": 1898}, + {"date": "2026-06-12", "stars": 1902}, + {"date": "2026-06-13", "stars": 1903}, + {"date": "2026-06-14", "stars": 1907}, + {"date": "2026-06-15", "stars": 1917}, + {"date": "2026-06-16", "stars": 1917}, + {"date": "2026-06-17", "stars": 1917}, + {"date": "2026-06-18", "stars": 1917}, + {"date": "2026-06-19", "stars": 1919}, + {"date": "2026-06-20", "stars": 1922}, + {"date": "2026-06-21", "stars": 1924}, + {"date": "2026-06-22", "stars": 1928}, + {"date": "2026-06-23", "stars": 1934}, + {"date": "2026-06-24", "stars": 1938}, + {"date": "2026-06-25", "stars": 1940}, + {"date": "2026-06-26", "stars": 1947}, + {"date": "2026-06-27", "stars": 1947}, + {"date": "2026-06-28", "stars": 1948}, + {"date": "2026-06-29", "stars": 1953}, + {"date": "2026-06-30", "stars": 1956}, + {"date": "2026-07-01", "stars": 1962}, + {"date": "2026-07-02", "stars": 1967}, + {"date": "2026-07-03", "stars": 1973}, + {"date": "2026-07-04", "stars": 1986}, + {"date": "2026-07-05", "stars": 1986}, + {"date": "2026-07-06", "stars": 1990}, + {"date": "2026-07-07", "stars": 1993}, + {"date": "2026-07-08", "stars": 1995}, + {"date": "2026-07-09", "stars": 1999}, + {"date": "2026-07-10", "stars": 2006}, + {"date": "2026-07-11", "stars": 2007}, + {"date": "2026-07-12", "stars": 2009}, + {"date": "2026-07-13", "stars": 2015}, + {"date": "2026-07-14", "stars": 2025}, + {"date": "2026-07-15", "stars": 2028}, + {"date": "2026-07-16", "stars": 2035}, + {"date": "2026-07-17", "stars": 2039}, + {"date": "2026-07-18", "stars": 2044}, + {"date": "2026-07-19", "stars": 2053}, + {"date": "2026-07-20", "stars": 2071}, + {"date": "2026-07-21", "stars": 2080}, + {"date": "2026-07-22", "stars": 2087}, + {"date": "2026-07-23", "stars": 2091}, + {"date": "2026-07-24", "stars": 2097}, + {"date": "2026-07-25", "stars": 2099}, + {"date": "2026-07-26", "stars": 2106}, + {"date": "2026-07-27", "stars": 2113}, + {"date": "2026-07-28", "stars": 2116}, + {"date": "2026-07-29", "stars": 2120}, + {"date": "2026-07-30", "stars": 2120}, + {"date": "2026-07-31", "stars": 2124}, + {"date": "2026-08-01", "stars": 2127}, + {"date": "2026-08-02", "stars": 2133}, + {"date": "2026-08-03", "stars": 2143}, + {"date": "2026-08-04", "stars": 2146}, + {"date": "2026-08-05", "stars": 2151}, + {"date": "2026-08-06", "stars": 2153}, + {"date": "2026-08-07", "stars": 2157}, + {"date": "2026-08-08", "stars": 2184}, + {"date": "2026-08-09", "stars": 2195}, + {"date": "2026-08-10", "stars": 2198}, + {"date": "2026-08-11", "stars": 2204}, + {"date": "2026-08-12", "stars": 2207} + ] +} diff --git a/.github/scripts/generate_star_history.py b/.github/scripts/generate_star_history.py index c22aef5f..c0a413be 100644 --- a/.github/scripts/generate_star_history.py +++ b/.github/scripts/generate_star_history.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate an SVG star-history chart for a GitHub repository.""" +"""Update a token-free daily star-count history and render it as SVG.""" from __future__ import annotations @@ -10,8 +10,7 @@ import sys import urllib.error import urllib.request -from collections import Counter -from datetime import date, datetime +from datetime import date, datetime, timezone from html import escape from pathlib import Path from typing import Any @@ -19,88 +18,115 @@ API_ROOT = "https://api.github.com" API_VERSION = "2026-03-10" -PER_PAGE = 100 -def parse_github_date(value: str) -> date: - return datetime.fromisoformat(value.replace("Z", "+00:00")).date() +def parse_date(value: str) -> date: + return date.fromisoformat(value) -def request_json(url: str, token: str, accept: str) -> Any: +def request_repository(repository: str) -> dict[str, Any]: + """Read public repository metadata, optionally using a short-lived token.""" + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "roboto-origin-star-history", + "X-GitHub-Api-Version": API_VERSION, + } + token = os.environ.get("GITHUB_TOKEN") + if token: + headers["Authorization"] = f"Bearer {token}" request = urllib.request.Request( - url, - headers={ - "Accept": accept, - "Authorization": f"Bearer {token}", - "User-Agent": "roboto-origin-star-history", - "X-GitHub-Api-Version": API_VERSION, - }, + f"{API_ROOT}/repos/{repository}", + headers=headers, ) try: with urllib.request.urlopen(request, timeout=30) as response: - return json.load(response) + data = json.load(response) except urllib.error.HTTPError as error: body = error.read().decode("utf-8", errors="replace") raise RuntimeError( - f"GitHub API request failed ({error.code}) for {url}: {body}" + f"GitHub API request failed ({error.code}): {body}" ) from error + except urllib.error.URLError as error: + raise RuntimeError(f"GitHub API request failed: {error.reason}") from error - -def fetch_repository(repository: str, token: str) -> dict[str, Any]: - data = request_json( - f"{API_ROOT}/repos/{repository}", token, "application/vnd.github+json" - ) if not isinstance(data, dict): raise RuntimeError("GitHub returned invalid repository metadata") return data -def fetch_stargazer_dates(repository: str, token: str) -> list[date]: - dates: list[date] = [] - page = 1 - while True: - url = ( - f"{API_ROOT}/repos/{repository}/stargazers" - f"?per_page={PER_PAGE}&page={page}" - ) - data = request_json(url, token, "application/vnd.github.star+json") - if not isinstance(data, list): - raise RuntimeError("GitHub returned an invalid stargazers response") - - for item in data: - starred_at = item.get("starred_at") if isinstance(item, dict) else None - if not starred_at: - raise RuntimeError( - "The stargazers response has no timestamps. Ensure the workflow " - "token can read repository metadata." - ) - dates.append(parse_github_date(starred_at)) - - if len(data) < PER_PAGE: - break - page += 1 - - return dates - - -def fetch_history(repository: str, token: str) -> tuple[date, list[date]]: - # A star can be added or removed during pagination. Retry once rather than - # publishing a chart that silently contains an incomplete history. - for attempt in range(2): - metadata = fetch_repository(repository, token) - created_at = parse_github_date(metadata["created_at"]) - starred_dates = fetch_stargazer_dates(repository, token) - current_metadata = fetch_repository(repository, token) - expected_count = int(current_metadata["stargazers_count"]) - if len(starred_dates) == expected_count: - return created_at, starred_dates - if attempt == 0: - continue +def load_history(path: Path, repository: str) -> tuple[date, list[tuple[date, int]]]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as error: + raise RuntimeError(f"Cannot read star history from {path}: {error}") from error + + if not isinstance(data, dict) or data.get("repository") != repository: + raise RuntimeError(f"{path} does not describe {repository}") + + try: + created_at = parse_date(data["created_at"]) + raw_snapshots = data["snapshots"] + except (KeyError, TypeError, ValueError) as error: + raise RuntimeError(f"{path} has invalid metadata") from error + + if not isinstance(raw_snapshots, list): + raise RuntimeError(f"{path} has invalid snapshots") + + snapshots: list[tuple[date, int]] = [] + for item in raw_snapshots: + try: + snapshot_day = parse_date(item["date"]) + stars = int(item["stars"]) + except (KeyError, TypeError, ValueError) as error: + raise RuntimeError(f"{path} has an invalid snapshot") from error + if stars < 0 or snapshot_day < created_at: + raise RuntimeError(f"{path} has an invalid snapshot") + snapshots.append((snapshot_day, stars)) + + if not snapshots: + raise RuntimeError(f"{path} contains no snapshots") + if snapshots != sorted(snapshots) or len({day for day, _ in snapshots}) != len( + snapshots + ): + raise RuntimeError(f"{path} snapshots must be unique and sorted") + return created_at, snapshots + + +def update_history( + snapshots: list[tuple[date, int]], snapshot_day: date, stars: int +) -> list[tuple[date, int]]: + if stars < 0: + raise RuntimeError("Star count cannot be negative") + if snapshot_day < snapshots[-1][0]: raise RuntimeError( - "Stargazer history changed while it was being fetched: " - f"received {len(starred_dates)} entries, expected {expected_count}." + f"Snapshot date {snapshot_day} precedes the latest stored snapshot " + f"{snapshots[-1][0]}" ) - raise AssertionError("unreachable") + if snapshot_day == snapshots[-1][0]: + return [*snapshots[:-1], (snapshot_day, stars)] + return [*snapshots, (snapshot_day, stars)] + + +def write_history( + path: Path, + repository: str, + created_at: date, + snapshots: list[tuple[date, int]], +) -> None: + data = { + "repository": repository, + "created_at": created_at.isoformat(), + "snapshots": [ + {"date": snapshot_day.isoformat(), "stars": stars} + for snapshot_day, stars in snapshots + ], + } + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(data, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) def y_axis(maximum: int, tick_count: int = 5) -> tuple[int, int]: @@ -122,25 +148,20 @@ def y_axis(maximum: int, tick_count: int = 5) -> tuple[int, int]: return ceiling, step -def history_points(created_at: date, starred_dates: list[date]) -> list[tuple[date, int]]: - points: list[tuple[date, int]] = [(created_at, 0)] - total = 0 - for day, count in sorted(Counter(starred_dates).items()): - total += count - points.append((day, total)) - return points - - -def render_svg(repository: str, created_at: date, starred_dates: list[date]) -> str: +def render_svg( + repository: str, + created_at: date, + snapshots: list[tuple[date, int]], +) -> str: width, height = 960, 520 left, right, top, bottom = 78, 28, 82, 66 plot_width = width - left - right plot_height = height - top - bottom - points = history_points(created_at, starred_dates) - first_day = min(day for day, _ in points) - last_day = max(day for day, _ in points) + points = [(created_at, 0), *snapshots] + first_day = created_at + last_day = snapshots[-1][0] day_span = max(1, (last_day - first_day).days) - maximum, y_step = y_axis(len(starred_dates)) + maximum, y_step = y_axis(max(stars for _, stars in points)) def x(day: date) -> float: return left + ((day - first_day).days / day_span) * plot_width @@ -181,10 +202,11 @@ def y(value: int) -> float: ) safe_repository = escape(repository) + latest_stars = snapshots[-1][1] last_x, last_y = coordinates[-1] return f''' Star history for {safe_repository} - {len(starred_dates)} stars since {created_at.isoformat()} + {latest_stars} stars as of {last_day.isoformat()} @@ -199,7 +221,7 @@ def y(value: int) -> float: Star History - {safe_repository} · {len(starred_dates)} stars + {safe_repository} · {latest_stars} stars · daily snapshots {''.join(y_grid)} {''.join(x_grid)} @@ -213,29 +235,69 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--repository", - default=os.environ.get("GITHUB_REPOSITORY", "Roboparty/roboto_origin"), + default="Roboparty/roboto_origin", help="GitHub repository in OWNER/REPO form", ) + parser.add_argument( + "--history", + default=".github/data/star-history.json", + help="Daily snapshot JSON file to update", + ) parser.add_argument( "--output", default="assets/star-history.svg", help="SVG output path" ) + parser.add_argument( + "--current-stars", + type=int, + help="Override the public API count for deterministic tests", + ) + parser.add_argument( + "--today", help="Override the UTC snapshot date for deterministic tests" + ) args = parser.parse_args() - token = os.environ.get("GITHUB_TOKEN") - if not token: - print("GITHUB_TOKEN is required", file=sys.stderr) - return 2 - - created_at, starred_dates = fetch_history(args.repository, token) - svg = render_svg(args.repository, created_at, starred_dates) - output = Path(args.output) - output.parent.mkdir(parents=True, exist_ok=True) - temporary = output.with_suffix(output.suffix + ".tmp") - temporary.write_text(svg, encoding="utf-8", newline="\n") - temporary.replace(output) - print(f"Wrote {output} with {len(starred_dates)} stars") + history_path = Path(args.history) + output_path = Path(args.output) + created_at, snapshots = load_history(history_path, args.repository) + + if args.current_stars is None: + metadata = request_repository(args.repository) + current_stars = int(metadata["stargazers_count"]) + repository_created_at = datetime.fromisoformat( + metadata["created_at"].replace("Z", "+00:00") + ).date() + if repository_created_at != created_at: + raise RuntimeError( + f"Stored creation date {created_at} does not match GitHub " + f"metadata {repository_created_at}" + ) + else: + current_stars = args.current_stars + + snapshot_day = ( + parse_date(args.today) + if args.today + else datetime.now(timezone.utc).date() + ) + snapshots = update_history(snapshots, snapshot_day, current_stars) + write_history(history_path, args.repository, created_at, snapshots) + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text( + render_svg(args.repository, created_at, snapshots), + encoding="utf-8", + newline="\n", + ) + print( + f"Wrote {output_path} and {history_path} with " + f"{current_stars} stars for {snapshot_day}" + ) return 0 if __name__ == "__main__": - raise SystemExit(main()) + try: + raise SystemExit(main()) + except RuntimeError as error: + print(error, file=sys.stderr) + raise SystemExit(1) from error diff --git a/.github/workflows/update-star-history.yml b/.github/workflows/update-star-history.yml index c5244f08..10463a1a 100644 --- a/.github/workflows/update-star-history.yml +++ b/.github/workflows/update-star-history.yml @@ -4,38 +4,107 @@ on: schedule: - cron: "17 2 * * *" workflow_dispatch: + push: + branches: + - main + paths: + - ".github/data/star-history.json" + - ".github/scripts/generate_star_history.py" + - ".github/workflows/update-star-history.yml" -permissions: - contents: write +permissions: {} concurrency: group: update-star-history cancel-in-progress: true jobs: - update: + render: runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read steps: - - name: Check out repository - uses: actions/checkout@v7.0.1 + - name: Check out source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Prepare snapshot history + shell: bash + run: | + set -euo pipefail + history_dir="$RUNNER_TEMP/star-history" + mkdir -p "$history_dir" + + if git fetch --quiet --depth=1 origin refs/heads/star-history; then + git show FETCH_HEAD:star-history.json > "$history_dir/star-history.json" + else + cp .github/data/star-history.json "$history_dir/star-history.json" + fi - - name: Generate chart + - name: Generate chart from public repository metadata env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ secrets.STAR_HISTORY_TOKEN }} - run: python3 .github/scripts/generate_star_history.py + # This job token is read-only, repository-scoped, and expires with the job. + GITHUB_TOKEN: ${{ github.token }} + run: | + python3 .github/scripts/generate_star_history.py \ + --history "$RUNNER_TEMP/star-history/star-history.json" \ + --output "$RUNNER_TEMP/star-history/star-history.svg" + + - name: Upload generated chart + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: star-history + path: | + ${{ runner.temp }}/star-history/star-history.json + ${{ runner.temp }}/star-history/star-history.svg + if-no-files-found: error + retention-days: 1 + + publish: + needs: render + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + + steps: + - name: Check out repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 1 + + - name: Download generated chart + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: star-history + path: ${{ runner.temp }}/star-history - - name: Commit updated chart + - name: Publish chart branch + shell: bash run: | - if git diff --quiet -- assets/star-history.svg; then + set -euo pipefail + generated_dir="$RUNNER_TEMP/star-history" + + if git fetch --quiet --depth=1 origin refs/heads/star-history; then + git switch --force-create star-history FETCH_HEAD + else + git switch --orphan star-history + git rm -rf --ignore-unmatch . + fi + + cp "$generated_dir/star-history.json" star-history.json + cp "$generated_dir/star-history.svg" star-history.svg + + if [[ -z "$(git status --porcelain -- star-history.json star-history.svg)" ]]; then echo "Star history is already up to date." exit 0 fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add assets/star-history.svg + git add star-history.json star-history.svg git commit -m "chore: update star history chart" - git push + git push origin HEAD:star-history diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e78d44a7..37ad329d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,7 +106,7 @@ When reporting bugs or requesting features: ### License -All contributions are subject to the GPLv3 license. By contributing, you agree that your contributions will be licensed under the same terms as the project. +New contributions made directly to the `roboto_origin` aggregation layer are accepted under GNU GPL version 3 unless the target file states otherwise. Module-level contributions must be submitted to the corresponding source repository and follow that repository's applicable contribution and license terms. This applies prospectively and does not relicense existing module snapshots or third-party material. See [LICENSES.md](LICENSES.md). ## Development Workflow diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index d0500ee8..70795ad7 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -106,7 +106,7 @@ git push origin feature/your-feature-name ### 许可证 -所有贡献均受 GPLv3 许可证约束。通过贡献,您同意您的贡献将按照与项目相同的条款进行许可。 +直接提交到 `roboto_origin` 聚合层的新贡献,除非目标文件另有说明,按 GNU GPL 第 3 版接收。模块级贡献必须提交到对应的来源仓库,并遵循该仓库适用的贡献规则和许可证条款。本规则仅面向未来,不会对现有模块快照或第三方内容进行重新许可。详见 [LICENSES.md](LICENSES.md)。 ## 开发工作流程 diff --git a/LICENSES.md b/LICENSES.md new file mode 100644 index 00000000..927729f3 --- /dev/null +++ b/LICENSES.md @@ -0,0 +1,52 @@ +# Licensing Scope + +This file explains the licensing boundaries of the `roboto_origin` aggregation repository. It does not replace any license text or change the license of any existing material. + +## Aggregation layer + +Unless a file states otherwise, RoboParty-authored aggregation-layer material outside `modules/` is provided under GNU General Public License version 3, as set out in the root [LICENSE](LICENSE) file. + +## Module snapshots and third-party material + +The directories under `modules/` are synchronized snapshots of separate repositories. Each snapshot and any third-party material within it remain subject to their own applicable copyright notices and license terms. Inclusion in this aggregation repository does not transfer ownership and does not relicense that material under the root `LICENSE`. + +| Snapshot path | Source repository | +| --- | --- | +| `modules/roboparty_deploy` | [Roboparty/roboparty_deploy](https://github.com/Roboparty/roboparty_deploy) | +| `modules/roboparty_firmware` | [Roboparty/roboparty_firmware](https://github.com/Roboparty/roboparty_firmware) | +| `modules/roboparty_navigation` | [Roboparty/roboparty_navigation](https://github.com/Roboparty/roboparty_navigation) | +| `modules/roboparty_train` | [Roboparty/roboparty_train](https://github.com/Roboparty/roboparty_train) | +| `modules/roboparty_xr_teleop` | [Roboparty/roboparty_xr_teleop](https://github.com/Roboparty/roboparty_xr_teleop) | +| `modules/rpo_appearance` | [Roboparty/rpo_appearance](https://github.com/Roboparty/rpo_appearance) | +| `modules/rpo_description` | [Roboparty/rpo_description](https://github.com/Roboparty/rpo_description) | +| `modules/rpo_hardware` | [Roboparty/rpo_hardware](https://github.com/Roboparty/rpo_hardware) | + +Within a module snapshot, the most specific file- or directory-level license notice takes precedence. If a snapshot does not contain sufficient license information, do not infer that the root GPL applies; consult the source repository before redistribution. + +## Contributions + +New contributions made directly to the aggregation layer are accepted under GNU GPL version 3 unless the target file states otherwise. Module-level contributions must be made to the corresponding source repository and follow that repository's applicable contribution and license terms. + +This policy applies prospectively to new contributions and does not relicense existing module snapshots or third-party material. + +--- + +# 许可证适用范围 + +本文说明 `roboto_origin` 聚合仓库中的许可证边界。本文不替代任何许可证正文,也不改变任何现有内容的许可证。 + +## 聚合层 + +除非具体文件另有说明,位于 `modules/` 之外、由 RoboParty 创作的聚合层内容,按根目录 [LICENSE](LICENSE) 所载的 GNU 通用公共许可证第 3 版提供。 + +## 模块快照和第三方内容 + +`modules/` 下的目录是独立仓库的同步快照。每个快照及其中的第三方内容,继续适用其各自的版权声明和许可证条款。将这些内容收录到本聚合仓库不表示所有权转移,也不会使其被根目录 `LICENSE` 重新许可。 + +各快照的来源仓库见上表。模块快照内应以适用范围最具体的文件级或目录级许可证声明为准。如果快照中没有充分的许可证信息,不应推定根目录 GPL 自动适用;再分发前应查阅对应的来源仓库。 + +## 贡献 + +直接提交到聚合层的新贡献,除非目标文件另有说明,按 GNU GPL 第 3 版接收。模块级贡献必须提交到对应的来源仓库,并遵循该仓库适用的贡献规则和许可证条款。 + +本规则仅面向未来的新贡献,不会对现有模块快照或第三方内容进行重新许可。 \ No newline at end of file diff --git a/README.md b/README.md index 0ca516ff..1ffb51a8 100644 --- a/README.md +++ b/README.md @@ -144,11 +144,11 @@ By using, modifying, or distributing this software, you acknowledge that you und --- -**This project is licensed under the GNU General Public License Version 3 (GPLv3). See [LICENSE](LICENSE) for details.** +**License:** RoboParty-authored aggregation-layer material is provided under GNU GPL version 3 unless otherwise stated. Snapshots under `modules/` and third-party material remain under their own applicable licenses and are not relicensed by the root `LICENSE`. See [LICENSES.md](LICENSES.md) for scope details. --- -[![Star History Chart](assets/star-history.svg)](https://github.com/Roboparty/roboto_origin/stargazers) +[![Star History Chart](https://raw.githubusercontent.com/Roboparty/roboto_origin/star-history/star-history.svg)](https://github.com/Roboparty/roboto_origin) --- diff --git a/README_cn.md b/README_cn.md index 73a2f240..be32740a 100644 --- a/README_cn.md +++ b/README_cn.md @@ -146,11 +146,11 @@ git pull --- -**本项目采用 GNU General Public License Version 3 (GPLv3) 许可证。详见 [LICENSE](LICENSE) 文件。** +**许可证:** 除非另有说明,由 RoboParty 创作的聚合层内容按 GNU GPL 第 3 版提供。`modules/` 下的快照及第三方内容继续适用其各自的许可证,不会被根目录 `LICENSE` 重新许可。适用范围详见 [LICENSES.md](LICENSES.md)。 --- -[![Star History Chart](assets/star-history.svg)](https://github.com/Roboparty/roboto_origin/stargazers) +[![Star History Chart](https://raw.githubusercontent.com/Roboparty/roboto_origin/star-history/star-history.svg)](https://github.com/Roboparty/roboto_origin) --- diff --git a/assets/star-history.svg b/assets/star-history.svg deleted file mode 100644 index d9f3588b..00000000 --- a/assets/star-history.svg +++ /dev/null @@ -1,24 +0,0 @@ - - Star history for Roboparty/roboto_origin - 2076 stars since 2025-12-31 - - - - - - - - - Star History - Roboparty/roboto_origin · 2076 stars - 05001000150020002500 - 2025-122026-022026-032026-052026-062026-07 - - - -