From 099edab66cbb5834050f310497a6b04dc7691f98 Mon Sep 17 00:00:00 2001 From: jammyfu Date: Sat, 7 Nov 2026 20:51:37 +0800 Subject: [PATCH 1/2] fix: allow delayed daily schedules to refresh stale rankings --- .github/workflows/daily-update.yml | 17 +++-- CURRENT_PLAN.md | 31 ++++----- TODO_BACKLOG.md | 2 +- docs/DAILY_UPDATE.md | 27 +++++--- docs/project-governance/ACCEPTANCE.md | 7 ++ docs/project-governance/CHANGELOG.md | 5 ++ docs/project-governance/DECISIONS.md | 8 +++ docs/project-governance/WORKLOG.md | 10 +++ tests/test_schedule.py | 94 ++++++++++++++++++++++++++- tools/schedule_guard.py | 56 +++++++++++++--- 10 files changed, 218 insertions(+), 39 deletions(-) diff --git a/.github/workflows/daily-update.yml b/.github/workflows/daily-update.yml index d66f783..386639e 100644 --- a/.github/workflows/daily-update.yml +++ b/.github/workflows/daily-update.yml @@ -1,11 +1,12 @@ name: daily-update # Each run also stores its result under rankings/// (history) and latest/. -# Runs once a day at America/Los_Angeles 00:00 (follows DST). +# Targets America/Los_Angeles 00:00 (follows DST); delayed runs catch up that local day. # Los Angeles midnight: 07:00 UTC in PDT, 08:00 UTC in PST -> '0 7 * * *' and '0 8 * * *' -# GitHub cron is UTC-only, so both entries fire and tools/schedule_guard.py lets only the -# one that is actually 00:xx in America/Los_Angeles continue; the other one skips. -# Scheduled workflows run from the default branch only. Runs can start a few minutes late. +# GitHub cron is UTC-only, so both entries fire. The guard checks the committed data's +# Los Angeles date rather than requiring execution during 00:xx: late jobs still refresh, +# and the second trigger skips once that local day's data has been committed. +# Scheduled workflows run from the default branch only and may start hours late. # If Actions is unavailable for the account, use tools/local_daily.sh (see docs/DAILY_UPDATE.md). on: schedule: @@ -33,13 +34,17 @@ jobs: env: EVENT: ${{ github.event_name }} run: | - # Scheduled runs proceed only at Los Angeles midnight (history folder pt-0000). + # Scheduled runs catch up if this Los Angeles day has no committed refresh (pt-0000). # Manual dispatch always proceeds: auto -> pt if the clock is at LA midnight, else manual-HHMM. if [ "$EVENT" = "schedule" ]; then - if python3 tools/schedule_guard.py --tz America/Los_Angeles --hour 0; then + if python3 tools/schedule_guard.py --tz America/Los_Angeles --daily-state data/stats.json; then echo "run=true" >> "$GITHUB_OUTPUT" echo "slot=pt" >> "$GITHUB_OUTPUT" else + status=$? + if [ "$status" -ne 1 ]; then + exit "$status" + fi echo "run=false" >> "$GITHUB_OUTPUT" fi else diff --git a/CURRENT_PLAN.md b/CURRENT_PLAN.md index 6ed7c8f..7e27704 100644 --- a/CURRENT_PLAN.md +++ b/CURRENT_PLAN.md @@ -2,38 +2,39 @@ ## Plan ID -PLAN-005 +PLAN-006 ## Goal -Production ranking updates run once a day at America/Los_Angeles midnight; public README copy only says it updates daily, with no cron clocks or dual Beijing+PT schedule. +Recover the stale ranking data and let delayed GitHub schedule runs catch up once per Los Angeles calendar day without duplicate refreshes. ## In Scope -- `.github/workflows/daily-update.yml`, `tools/schedule_guard.py` wiring, `docs/launchd/`, `tools/local_daily.sh`, `tools/mac_i18n_cron.sh`, `tools/com.jammyfu.risers-i18n.plist` -- Public copy in `tools/i18n.py` (stamp / a3 / schedule / tagline), `docs/FAQ.md`, `docs/FACTS.md`; operator details in `docs/DAILY_UPDATE.md` -- Tests in `tests/test_schedule.py`, `tests/test_spam.py`, `tests/test_repo.py`; governance files +- `tools/schedule_guard.py`, `.github/workflows/daily-update.yml`, `tests/test_schedule.py` +- `docs/DAILY_UPDATE.md` and project governance records +- Manual execution and verification of the existing production data workflow ## Out of Scope -- Ranking logic, star-data fetch, translation pipeline, deleting historical `cn-0000` snapshots, merging to `main` +- Ranking calculations, generated data edits, translations, cron frequency, local launchd settings, merging to `main` ## Tasks -- [x] Drop the Beijing cron from `daily-update.yml`; keep the two LA UTC crons with `schedule_guard.py`, slot `pt` -- [x] Align local fallback (launchd / cron docs / i18n Mac job) to once daily after US midnight -- [x] Change README / HOW_IT_WORKS strings so readers only see daily update / 每天更新, not clocks -- [x] Update FAQ, FACTS, operator docs and tests that pinned twice-daily / Beijing 00:00 -- [x] Re-render generated pages; `python3 tools/verify.py` and `python3 tools/next_plan.py --check` pass -- [x] Open a PR; do not merge +- [x] Diagnose skipped schedule runs and start the existing manual data update +- [x] Add regression tests for late runs, same-day deduplication, DST, invalid state and workflow error propagation +- [x] Gate scheduled refreshes on the committed data's Los Angeles date, preserving manual dispatch and local-hour mode +- [x] Run full verification and document operator behavior and follow-up +- [x] Verify production refresh run 42373937914 and committed data at `2656f40` +- [ ] Owner reviews the delayed-schedule PR; merge only with approval ## Verification - `python3 tools/verify.py` +- `python3 tools/verify.py --daily` - `python3 tools/next_plan.py --check` ## Next Candidates -- After merge, confirm the next scheduled `daily-update` run writes `rankings//pt-0000/` -- Thin Developer Tools / Design / Data new-repo boards: see `TODO_BACKLOG.md` -- Acceleration column (this week vs the week before) +- After owner-approved merge, verify a delayed scheduled run publishes data and the second trigger skips +- Monitor Pages freshness separately from the data workflow if the bot commit does not trigger deployment +- Thin newcomer boards and acceleration column remain in `TODO_BACKLOG.md` diff --git a/TODO_BACKLOG.md b/TODO_BACKLOG.md index 4817b26..1a3999e 100644 --- a/TODO_BACKLOG.md +++ b/TODO_BACKLOG.md @@ -2,7 +2,7 @@ ## Now -- [ ] After the once-daily US schedule PR (PLAN-005) is merged, confirm the next scheduled `daily-update` run writes `rankings//pt-0000/` (`gh run list --workflow daily-update.yml`) +- [ ] Review PLAN-006 delayed-schedule fix; after owner-approved merge, confirm a late `daily-update` writes `rankings//pt-0000/` and the next same-day trigger skips (`gh run list --workflow daily-update.yml`) - [ ] Developer Tools new-repo board: 15 of 20 entries were flagged ⚠ in the last run because star-farmed repos use `cli` / `developer-tools`; decide whether to add a quality signal (owner call) - [ ] Design and Data new-repo boards are thin (0-2 entries): widen with specific topics only (no generic ones; `tests/test_spam.py` guards that) diff --git a/docs/DAILY_UPDATE.md b/docs/DAILY_UPDATE.md index e16524b..c862dd1 100644 --- a/docs/DAILY_UPDATE.md +++ b/docs/DAILY_UPDATE.md @@ -77,14 +77,25 @@ the Antigravity CLI (`tools/polish_local.sh`, or `tools/local_daily.sh`), never | America/Los_Angeles 00:00, PDT (UTC-7) | 07:00 UTC | `0 7 * * *` | | America/Los_Angeles 00:00, PST (UTC-8) | 08:00 UTC | `0 8 * * *` | -GitHub cron only knows UTC, so both LA entries fire every day. `tools/schedule_guard.py --tz America/Los_Angeles --hour 0` -checks the real Los Angeles clock and lets only the one that is actually 00:xx there continue; the other run skips -(you will see a green run whose steps are skipped). Tests cover PDT, PST, both 2026 DST changes (Mar 8, Nov 1) and -assert that exactly one of the two LA slots passes on every day of the year (`tests/test_schedule.py`). -Manual `workflow_dispatch` always runs. - -Caveat: GitHub may start scheduled runs several minutes late. A run delayed past the end of the 00:xx hour in Los Angeles -would skip until the next day's slot. Use `workflow_dispatch` (or `tools/local_daily.sh`) if a day needs a manual refresh. +GitHub cron only knows UTC, so both LA entries fire every day. Actions uses +`tools/schedule_guard.py --tz America/Los_Angeles --daily-state data/stats.json` to compare +`generated_at` in the committed data with the current **Los Angeles calendar date**. If today's +refresh is missing, it runs even hours after midnight. Once a refresh for that date is committed, +the second trigger skips. A manual refresh on the same LA day also counts; manual +`workflow_dispatch` itself always runs when requested. + +The `daily-update` concurrency group serializes runs, and each job checks out current `main` +before evaluating the gate. A failed fetch/verify/push does not advance the committed timestamp, +so the next trigger can retry. Missing state permits an initial refresh; corrupt, timezone-less or +future timestamps fail the job instead of appearing as a successful skip. Regression tests execute +the real gate shell with stale/current/invalid state and cover PST, PDT, and both DST transitions. +Local `--slot la` retains its hour-based guard; this change is for delayed GitHub Actions runs. + +The cron times are targets, not guaranteed start times. On 2026-11-06 both jobs started roughly +seven hours late and the old hour-only guard skipped all data steps. Delayed runs now use their +actual execution day's `pt-0000` archive slot and record the real `generated_at`; no historical +counts or missed-day snapshots are invented. If GitHub drops both triggers entirely, use +`workflow_dispatch` (or `tools/local_daily.sh`) for a manual refresh. ## Option A: GitHub Actions (preferred when it runs) diff --git a/docs/project-governance/ACCEPTANCE.md b/docs/project-governance/ACCEPTANCE.md index 193eda4..db00d53 100644 --- a/docs/project-governance/ACCEPTANCE.md +++ b/docs/project-governance/ACCEPTANCE.md @@ -29,3 +29,10 @@ - `CURRENT_PLAN.md` passes `python3 tools/next_plan.py --check` (one Plan ID, one Goal, 3-7 tasks, Out of Scope, Verification, Next Candidates) - `tools/verify.py` and `tools/local_daily.sh` do not depend on the shell PATH or `shell=True` (`tests/test_harness.py`) - The harness files from `continuous-project-loop` exist and `AGENTS.md` names the two CoderSkills skills + + +## Delayed daily schedules + +- A scheduled run delayed beyond midnight refreshes when no data is committed for its current Los Angeles day. +- A second trigger on the same LA day skips after a successful commit, including across UTC date and DST boundaries. +- Bad/future scheduling state produces a failed job, not a green skip; manual dispatch stays unconditional. diff --git a/docs/project-governance/CHANGELOG.md b/docs/project-governance/CHANGELOG.md index 7c7bb4e..9bf6c60 100644 --- a/docs/project-governance/CHANGELOG.md +++ b/docs/project-governance/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG.md +## 2026-11-07 (delayed schedule recovery) + +- Scheduled updates can catch up after GitHub starts a job late. The committed data date prevents a second scheduled refresh on the same Los Angeles day. +- Invalid scheduling state now fails visibly instead of reporting a green skip. Manual dispatch and local fallback behavior are preserved. + ## 2026-11-06 (once-daily US schedule) - The ranking list updates once a day at America/Los_Angeles midnight (`0 7 * * *` / `0 8 * * *` UTC, DST-aware via `tools/schedule_guard.py`). The Beijing 00:00 cron is no longer a production slot. diff --git a/docs/project-governance/DECISIONS.md b/docs/project-governance/DECISIONS.md index 6737d6b..b73c887 100644 --- a/docs/project-governance/DECISIONS.md +++ b/docs/project-governance/DECISIONS.md @@ -92,3 +92,11 @@ - Production cadence is one ranking update per day, not Beijing 00:00 plus Los Angeles 00:00. - GitHub cron is still UTC-only, so both `0 7 * * *` (PDT) and `0 8 * * *` (PST) stay; `tools/schedule_guard.py` lets exactly one through. Historical `cn-0000` snapshots are kept; new scheduled runs write `pt-0000`. - Public README / FAQ / FACTS / HOW_IT_WORKS copy only says the list is updated daily. Exact clocks stay in `docs/DAILY_UPDATE.md` for operators. + + +## 2026-11-07 — tolerate delayed GitHub schedules (PLAN-006) + +- The target remains Los Angeles midnight with the existing two UTC crons. Use the committed `generated_at` converted to LA's calendar date to decide if a refresh is due, rather than the runner's current hour. +- Any successfully committed refresh on that LA date suppresses scheduled duplicates, including manual refreshes. Manual dispatch remains unconditional; local hour-gated fallback is unchanged. +- Keep workflow concurrency and fresh `main` checkout. Invalid/future state fails visibly; only exit 1 means an intentional skip. Do not backfill missed snapshots with today's numbers. +- Code changes remain a draft PR until the owner approves merge. The immediate refresh uses the existing production workflow. diff --git a/docs/project-governance/WORKLOG.md b/docs/project-governance/WORKLOG.md index ed464f9..be506c9 100644 --- a/docs/project-governance/WORKLOG.md +++ b/docs/project-governance/WORKLOG.md @@ -78,3 +78,13 @@ - Added FAQ q4/q5 (compute; use/contribute) linking existing GEO FAQ anchors; footer links `CONTRIBUTING.md`. - Test: templates contain no `YYYY-MM-DD`; rendered stamp follows `generated_at`; badge URL has no baked date. - Verification: `python3 tools/verify.py` green (202 tests). + + +## 2026-11-07 — delayed schedule recovery (PLAN-006) + +- Runs 42235915488 and 42243101773 were green but skipped fetch/verify/commit: runner times were 14:39/15:26 UTC, outside the old LA 00:xx gate. Main data had last refreshed on 2026-11-04. +- Production workflow run [42373937914](https://github.com/jammyfu/github-risers/actions/runs/42373937914) succeeded, committing `2656f40` with data generated at 2026-11-07T12:46:19Z. All 70 projects and seven newcomer categories fetched successfully; `latest/stats.json` matches `data/stats.json`, and daily verification reported zero data warnings. No generated data is hand-edited or backfilled here. +- Added a committed-data local-date gate for Actions, same-day deduplication, visible state errors, and real CLI/shell regression tests. Manual dispatch and local-hour mode are unchanged. +- Baseline: full verification passed with 222 tests. Observed the late-workflow regression fail (`run=false` instead of `run=true`) before implementation. Final full verification passed with 231 tests; `verify.py --daily` passed for all 70 committed projects with no data warnings; `next_plan.py --check` and `git diff --check` passed. +- Independent code review found no actionable issues; all 21 schedule tests also passed independently. A full-year simulation of both cron triggers delayed seven hours produced exactly one refresh per LA day on all 365 days of 2026. +- Next: owner review/approval of the draft code PR, then verify an unattended delayed scheduled update and same-day duplicate skip. No merge to main is performed in this round. diff --git a/tests/test_schedule.py b/tests/test_schedule.py index 389f293..7427481 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -1,8 +1,11 @@ import datetime as dt +import json import os import re import subprocess import sys +import tempfile +import shutil import unittest from pathlib import Path @@ -68,6 +71,95 @@ def test_exit_codes(self): self.assertIn("skip:", skip.stdout) +class DailyStateTests(unittest.TestCase): + def run_daily(self, now, state): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "stats.json" + if state is not None: + path.write_text(json.dumps(state), encoding="utf-8") + return subprocess.run( + [sys.executable, str(ROOT / "tools/schedule_guard.py"), "--tz", LA, + "--daily-state", str(path), "--now", now], capture_output=True, text=True) + + def test_seven_hour_delay_still_refreshes_stale_data(self): + r = self.run_daily("2026-11-06T15:26:58Z", {"generated_at": "2026-11-04T23:50:00Z"}) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("run:", r.stdout) + + def test_second_delayed_trigger_skips_same_la_day(self): + r = self.run_daily("2026-11-06T16:26:58Z", {"generated_at": "2026-11-06T15:26:58Z"}) + self.assertEqual(r.returncode, 1, r.stderr) + self.assertIn("skip:", r.stdout) + + def test_utc_date_change_does_not_count_as_a_new_la_day(self): + r = self.run_daily("2026-11-07T07:00:00Z", {"generated_at": "2026-11-06T15:26:58Z"}) + self.assertEqual(r.returncode, 1, r.stderr) + + def test_la_midnight_starts_a_new_day_across_dst_changes(self): + cases = [ + ("2026-03-08T08:00:00Z", "2026-03-07T08:00:00Z"), + ("2026-03-09T07:00:00Z", "2026-03-08T08:00:00Z"), + ("2026-11-01T07:00:00Z", "2026-10-31T07:00:00Z"), + ("2026-11-02T08:00:00Z", "2026-11-01T07:00:00Z"), + ] + for now, previous in cases: + with self.subTest(now=now): + r = self.run_daily(now, {"generated_at": previous}) + self.assertEqual(r.returncode, 0, r.stderr) + + def test_missing_state_allows_initial_refresh(self): + r = self.run_daily("2026-11-06T15:26:58Z", None) + self.assertEqual(r.returncode, 0, r.stderr) + + def test_bad_and_future_state_are_errors_not_successful_skips(self): + for state in ({}, [], {"generated_at": "invalid"}, + {"generated_at": "2026-11-06T08:00:00"}, + {"generated_at": "2026-11-07T08:00:00Z"}): + with self.subTest(state=state): + r = self.run_daily("2026-11-06T15:26:58Z", state) + self.assertEqual(r.returncode, 2, r.stderr) + self.assertIn("error:", r.stderr) + + +class WorkflowGateBehaviorTests(unittest.TestCase): + def run_gate(self, event, state): + workflow = (ROOT / ".github/workflows/daily-update.yml").read_text("utf-8") + gate = workflow.split(" run: |\n", 1)[1].split(" # Optional:", 1)[0] + script = "\n".join(line[10:] for line in gate.splitlines()) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "tools").mkdir() + shutil.copy(ROOT / "tools/schedule_guard.py", root / "tools/schedule_guard.py") + (root / "data").mkdir() + (root / "data/stats.json").write_text(json.dumps(state), encoding="utf-8") + output = root / "output" + env = dict(os.environ, EVENT=event, GITHUB_OUTPUT=str(output), + ATLAS_NOW_UTC="2026-11-06T15:26:58Z") + r = subprocess.run(["/bin/bash", "-e", "-c", script], cwd=root, env=env, + capture_output=True, text=True) + return r, output.read_text() if output.exists() else "" + + def test_delayed_schedule_fetches_and_duplicate_skips(self): + r, output = self.run_gate("schedule", {"generated_at": "2026-11-04T23:50:00Z"}) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("run=true", output) + self.assertIn("slot=pt", output) + r, output = self.run_gate("schedule", {"generated_at": "2026-11-06T09:00:00Z"}) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("run=false", output) + + def test_bad_state_fails_the_workflow_instead_of_reporting_success(self): + r, output = self.run_gate("schedule", {"generated_at": "invalid"}) + self.assertEqual(r.returncode, 2, r.stderr) + self.assertNotIn("run=false", output) + + def test_manual_dispatch_always_proceeds(self): + r, output = self.run_gate("workflow_dispatch", {"generated_at": "invalid"}) + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("run=true", output) + self.assertIn("slot=auto", output) + + class LocalScriptTests(unittest.TestCase): def run_script(self, now, slot): env = {k: v for k, v in os.environ.items() if k not in ("GITHUB_TOKEN", "GH_TOKEN")} @@ -100,7 +192,7 @@ def test_two_la_crons(self): self.assertNotIn("0 16 * * *", self.wf) def test_la_guard_wired_and_steps_gated(self): - self.assertIn("tools/schedule_guard.py --tz America/Los_Angeles --hour 0", self.wf) + self.assertIn("tools/schedule_guard.py --tz America/Los_Angeles --daily-state data/stats.json", self.wf) self.assertEqual(self.wf.count("if: steps.gate.outputs.run == 'true'"), 4) # install CLI, fetch, verify, commit self.assertNotIn("slot=cn", self.wf) # scheduled production is pt only self.assertIn("slot=pt", self.wf) diff --git a/tools/schedule_guard.py b/tools/schedule_guard.py index 69d89be..a234b83 100644 --- a/tools/schedule_guard.py +++ b/tools/schedule_guard.py @@ -1,18 +1,24 @@ #!/usr/bin/env python3 -"""Exit 0 when the current local hour in a time zone equals --hour, else exit 1. +"""Gate refreshes by local hour (local fallback) or by last successful data date (Actions). -Why: GitHub cron is UTC-only. America/Los_Angeles midnight is 07:00 UTC during PDT and 08:00 UTC -during PST, so the workflow schedules both and this guard lets exactly one of them proceed. +Actions can start hours late. --daily-state compares generated_at from the committed data +with the current date in --tz; only a successful refresh on that local date suppresses it. +The two UTC cron entries still target LA midnight across DST. Workflow concurrency and +checkout of current main ensure a later trigger observes the earlier committed refresh. + python3 tools/schedule_guard.py --tz America/Los_Angeles --daily-state data/stats.json python3 tools/schedule_guard.py --tz America/Los_Angeles --hour 0 - python3 tools/schedule_guard.py --tz America/Los_Angeles --hour 0 --now 2026-11-02T08:00:00Z # for tests + +Exit codes: 0 = run, 1 = intentional skip, 2 = invalid state/configuration. """ from __future__ import annotations import argparse import datetime as dt +import json import os import sys +from pathlib import Path from zoneinfo import ZoneInfo @@ -29,18 +35,52 @@ def is_local_hour(now_utc: dt.datetime, tz: str, hour: int) -> bool: def parse_now(value: str | None) -> dt.datetime: if not value: return dt.datetime.now(dt.timezone.utc) - return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(dt.timezone.utc) + parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + raise ValueError("timestamp must include a timezone") + return parsed.astimezone(dt.timezone.utc) + + +def daily_update_due(now_utc: dt.datetime, tz: str, state_path: Path) -> bool: + if now_utc.tzinfo is None: + raise ValueError("now_utc must be timezone-aware") + zone = ZoneInfo(tz) + try: + state = json.loads(state_path.read_text(encoding="utf-8")) + except FileNotFoundError: + return True + generated = state["generated_at"] + if not isinstance(generated, str) or not generated: + raise ValueError("generated_at must be a nonempty timezone-aware timestamp") + previous = parse_now(generated) + if previous > now_utc: + raise ValueError("generated_at is in the future") + return previous.astimezone(zone).date() < now_utc.astimezone(zone).date() def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--tz", required=True) - ap.add_argument("--hour", type=int, required=True) + mode = ap.add_mutually_exclusive_group(required=True) + mode.add_argument("--hour", type=int) + mode.add_argument("--daily-state", type=Path, help="committed stats.json used for once-per-local-day refresh") ap.add_argument("--now", default=os.environ.get("ATLAS_NOW_UTC"), help="override current time (ISO 8601, UTC); also read from ATLAS_NOW_UTC") args = ap.parse_args(argv) - now = parse_now(args.now) - h = local_hour(now, args.tz) + try: + now = parse_now(args.now) + if args.daily_state is not None: + due = daily_update_due(now, args.tz, args.daily_state) + day = now.astimezone(ZoneInfo(args.tz)).date() + if due: + print(f"run: no committed refresh for {day} in {args.tz}; late runs may catch up") + return 0 + print(f"skip: committed data already refreshed on {day} in {args.tz}") + return 1 + h = local_hour(now, args.tz) + except (ValueError, TypeError, KeyError, OSError) as exc: + print(f"error: cannot decide refresh: {exc}", file=sys.stderr) + return 2 if h == args.hour: print(f"run: {now:%Y-%m-%d %H:%M}Z is {args.hour:02d}:xx in {args.tz}") return 0 From a68e8fb83535f62cfd169090d389775fdf836b49 Mon Sep 17 00:00:00 2001 From: jammyfu Date: Sat, 7 Nov 2026 21:05:49 +0800 Subject: [PATCH 2/2] docs: align public Pages copy with daily updates --- CURRENT_PLAN.md | 10 ++++++---- TODO_BACKLOG.md | 2 +- docs/index.html | 14 +++++++------- docs/project-governance/CHANGELOG.md | 1 + docs/project-governance/DECISIONS.md | 2 +- docs/project-governance/WORKLOG.md | 8 ++++++++ docs/zh-Hans/index.html | 14 +++++++------- 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/CURRENT_PLAN.md b/CURRENT_PLAN.md index 7e27704..ea50b90 100644 --- a/CURRENT_PLAN.md +++ b/CURRENT_PLAN.md @@ -6,17 +6,18 @@ PLAN-006 ## Goal -Recover the stale ranking data and let delayed GitHub schedule runs catch up once per Los Angeles calendar day without duplicate refreshes. +Recover the stale ranking data and let delayed GitHub schedule runs catch up once per Los Angeles calendar day without duplicate refreshes, with consistent daily-update copy on both public landing pages. ## In Scope - `tools/schedule_guard.py`, `.github/workflows/daily-update.yml`, `tests/test_schedule.py` -- `docs/DAILY_UPDATE.md` and project governance records +- `docs/DAILY_UPDATE.md`, English/Simplified Chinese Pages copy, and project governance records +- Owner-authorized merge of PR #19 and deployment verification - Manual execution and verification of the existing production data workflow ## Out of Scope -- Ranking calculations, generated data edits, translations, cron frequency, local launchd settings, merging to `main` +- Ranking calculations, generated data edits, translations, cron frequency, local launchd settings ## Tasks @@ -25,7 +26,8 @@ Recover the stale ranking data and let delayed GitHub schedule runs catch up onc - [x] Gate scheduled refreshes on the committed data's Los Angeles date, preserving manual dispatch and local-hour mode - [x] Run full verification and document operator behavior and follow-up - [x] Verify production refresh run 42373937914 and committed data at `2656f40` -- [ ] Owner reviews the delayed-schedule PR; merge only with approval +- [x] Correct both Pages descriptions and metadata; verify the final PR head +- [ ] Verify the first unattended schedule run after the authorized merge; confirm no same-day duplicate ## Verification diff --git a/TODO_BACKLOG.md b/TODO_BACKLOG.md index 1a3999e..f69d20a 100644 --- a/TODO_BACKLOG.md +++ b/TODO_BACKLOG.md @@ -2,7 +2,7 @@ ## Now -- [ ] Review PLAN-006 delayed-schedule fix; after owner-approved merge, confirm a late `daily-update` writes `rankings//pt-0000/` and the next same-day trigger skips (`gh run list --workflow daily-update.yml`) +- [ ] After the authorized PLAN-006 merge, confirm a late `daily-update` writes `rankings//pt-0000/` and the next same-day trigger skips (`gh run list --workflow daily-update.yml`) - [ ] Developer Tools new-repo board: 15 of 20 entries were flagged ⚠ in the last run because star-farmed repos use `cli` / `developer-tools`; decide whether to add a quality signal (owner call) - [ ] Design and Data new-repo boards are thin (0-2 entries): widen with specific topics only (no generic ones; `tests/test_spam.py` guards that) diff --git a/docs/index.html b/docs/index.html index cc33946..2329c83 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,14 +4,14 @@ GitHub Risers: GitHub Trending Repos Ranked by Star Growth - + - + @@ -27,7 +27,7 @@ "@context": "https://schema.org", "@type": "Dataset", "name": "GitHub Risers: GitHub repositories ranked by star growth", - "description": "GitHub trending repositories ranked by stars gained per day, not total stars. AI first, plus developer tools, security, web, infrastructure, design and data. Updated twice a day from the GitHub API.", + "description": "GitHub trending repositories ranked by stars gained per day, not total stars. AI first, plus developer tools, security, web, infrastructure, design and data. Updated daily from the GitHub API.", "url": "https://jammyfu.github.io/github-risers/", "sameAs": "https://github.com/jammyfu/github-risers", "license": "https://opensource.org/license/mit", @@ -116,7 +116,7 @@ "name": "How often is it updated?", "acceptedAnswer": { "@type": "Answer", - "text": "Twice a day, at 00:00 Beijing time (UTC+8) and 00:00 Los Angeles time. The schedule runs on GitHub Actions, so a run can start a few minutes late. Every run is archived under its own folder in the repository." + "text": "Updated daily. Every run is archived under its own folder in the repository." } }, { @@ -166,7 +166,7 @@

GitHub Risers

-

GitHub trending repositories ranked by star growth (stars gained per day), not total stars. AI is the main board; developer tools, security, web, infrastructure, design and data have their own. Numbers come straight from the GitHub API and are refreshed twice a day.

+

GitHub trending repositories ranked by star growth (stars gained per day), not total stars. AI is the main board; developer tools, security, web, infrastructure, design and data have their own. Numbers come straight from the GitHub API and are refreshed daily.

It is a script-generated leaderboard: not an official GitHub product, not a manually curated list, and not a star-buying or promotion service.

Open the latest rankings

简体中文

@@ -174,14 +174,14 @@

GitHub Risers

Key facts

- +
WhatRepositories ranked by stars gained per day over 7, 30 and 365 days, plus a 7-day hottest-new-repos board
CategoriesAI (main), Developer Tools, Security, Web / Frontend, Infrastructure, Design / Creative, Data
Updates00:00 Beijing time and 00:00 Los Angeles time, every day
Data sourceGitHub REST API only; missing data is shown as missing, never estimated
TelemetryNone
LicenseMIT
WhatRepositories ranked by stars gained per day over 7, 30 and 365 days, plus a 7-day hottest-new-repos board
CategoriesAI (main), Developer Tools, Security, Web / Frontend, Infrastructure, Design / Creative, Data
UpdatesDaily
Data sourceGitHub REST API only; missing data is shown as missing, never estimated
TelemetryNone
LicenseMIT

Rankings in five languages

FAQ

-
What is GitHub Risers?

A leaderboard of GitHub repositories ranked by how fast they gain stars, not by how many they already have. AI is the main board, with separate boards for developer tools, security, web, infrastructure, design and data.

How is the growth rate computed?

per_day = stars gained over the window / days used. The gain is the sum of daily star counts from the GitHub API endpoint stargazers/history over the complete UTC days before the run. A repository younger than the window is divided by its own age and marked young.

How often is it updated?

Twice a day, at 00:00 Beijing time (UTC+8) and 00:00 Los Angeles time. The schedule runs on GitHub Actions, so a run can start a few minutes late. Every run is archived under its own folder in the repository.

Is it the same as GitHub Trending?

No. GitHub Trending is GitHub's own page. GitHub Risers is an independent open-source project with published rules and a saved history of every update. It is not affiliated with GitHub.

What is the hottest new repos board?

Repositories created in the last 7 days with at least 100 stars, not forks, not archived, ranked by stars divided by age in days.

Does it detect fake stars?

No. It reports the counts the GitHub API returns. It only flags, with a published heuristic, new repos that look like cracked or pirated bundles and ranks them after all normal repos.

Does it use generative AI?

The rankings are computed by plain scripts. Translations of repo descriptions can be produced with an AI model in an optional step; numbers never are.

How do I cite it?

Use the CITATION.cff file in the repository (GitHub shows a Cite this repository button) and name the snapshot date.

+
What is GitHub Risers?

A leaderboard of GitHub repositories ranked by how fast they gain stars, not by how many they already have. AI is the main board, with separate boards for developer tools, security, web, infrastructure, design and data.

How is the growth rate computed?

per_day = stars gained over the window / days used. The gain is the sum of daily star counts from the GitHub API endpoint stargazers/history over the complete UTC days before the run. A repository younger than the window is divided by its own age and marked young.

How often is it updated?

Updated daily. Every run is archived under its own folder in the repository.

Is it the same as GitHub Trending?

No. GitHub Trending is GitHub's own page. GitHub Risers is an independent open-source project with published rules and a saved history of every update. It is not affiliated with GitHub.

What is the hottest new repos board?

Repositories created in the last 7 days with at least 100 stars, not forks, not archived, ranked by stars divided by age in days.

Does it detect fake stars?

No. It reports the counts the GitHub API returns. It only flags, with a published heuristic, new repos that look like cracked or pirated bundles and ranks them after all normal repos.

Does it use generative AI?

The rankings are computed by plain scripts. Translations of repo descriptions can be produced with an AI model in an optional step; numbers never are.

How do I cite it?

Use the CITATION.cff file in the repository (GitHub shows a Cite this repository button) and name the snapshot date.

Documentation

  • Facts (one page)
  • FAQ
  • Category rules
  • New-repo criteria
  • Daily update
  • Latest data (JSON)
  • llms.txt
  • Source code
  • diff --git a/docs/project-governance/CHANGELOG.md b/docs/project-governance/CHANGELOG.md index 9bf6c60..e202f95 100644 --- a/docs/project-governance/CHANGELOG.md +++ b/docs/project-governance/CHANGELOG.md @@ -4,6 +4,7 @@ - Scheduled updates can catch up after GitHub starts a job late. The committed data date prevents a second scheduled refresh on the same Los Angeles day. - Invalid scheduling state now fails visibly instead of reporting a green skip. Manual dispatch and local fallback behavior are preserved. +- English and Simplified Chinese landing pages now consistently say daily, including search/social descriptions and FAQ structured metadata. ## 2026-11-06 (once-daily US schedule) diff --git a/docs/project-governance/DECISIONS.md b/docs/project-governance/DECISIONS.md index b73c887..f37e356 100644 --- a/docs/project-governance/DECISIONS.md +++ b/docs/project-governance/DECISIONS.md @@ -99,4 +99,4 @@ - The target remains Los Angeles midnight with the existing two UTC crons. Use the committed `generated_at` converted to LA's calendar date to decide if a refresh is due, rather than the runner's current hour. - Any successfully committed refresh on that LA date suppresses scheduled duplicates, including manual refreshes. Manual dispatch remains unconditional; local hour-gated fallback is unchanged. - Keep workflow concurrency and fresh `main` checkout. Invalid/future state fails visibly; only exit 1 means an intentional skip. Do not backfill missed snapshots with today's numbers. -- Code changes remain a draft PR until the owner approves merge. The immediate refresh uses the existing production workflow. +- The immediate refresh used the existing production workflow. The owner subsequently authorized merging PR #19 and correcting the stale Pages schedule copy. Public English and Simplified Chinese copy says daily without exact clock claims; visible text and structured metadata stay aligned. diff --git a/docs/project-governance/WORKLOG.md b/docs/project-governance/WORKLOG.md index be506c9..af7a200 100644 --- a/docs/project-governance/WORKLOG.md +++ b/docs/project-governance/WORKLOG.md @@ -88,3 +88,11 @@ - Baseline: full verification passed with 222 tests. Observed the late-workflow regression fail (`run=false` instead of `run=true`) before implementation. Final full verification passed with 231 tests; `verify.py --daily` passed for all 70 committed projects with no data warnings; `next_plan.py --check` and `git diff --check` passed. - Independent code review found no actionable issues; all 21 schedule tests also passed independently. A full-year simulation of both cron triggers delayed seven hours produced exactly one refresh per LA day on all 365 days of 2026. - Next: owner review/approval of the draft code PR, then verify an unattended delayed scheduled update and same-day duplicate skip. No merge to main is performed in this round. + + +## 2026-11-07 — authorized merge and Pages copy follow-up + +- Owner authorized merging PR #19 and correcting the obsolete twice-daily Pages descriptions. Rechecked main (`2656f40`), the previously reviewed PR head (`099edab`), and successful CI before extending the same PR. +- Updated English and Simplified Chinese Pages body copy, facts table, FAQ, search/social metadata and JSON-LD to daily, matching the current public README/FAQ wording. No ranking data or layout changes. +- Page verification first failed on the old structured description, then passed for both languages after correction; JSON-LD is valid and the visible FAQ matches its structured answer. Full verification passed with 231 tests; daily verification, plan validation and diff checks passed. +- Merge the exact verified final head after its CI passes. No formal review approval is submitted. Next operational check: confirm main CI/Pages deployment, then observe the next unattended schedule run for refresh and duplicate suppression. diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index 939336e..af431b4 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -4,14 +4,14 @@ GitHub 涨星榜(GitHub Risers):按涨星速度排名的 GitHub 热门仓库 - + - + @@ -27,7 +27,7 @@ "@context": "https://schema.org", "@type": "Dataset", "name": "GitHub 涨星榜(GitHub Risers)", - "description": "按每天新增 star 数(涨星速度)而不是总 star 数给 GitHub 仓库排名。AI 为主榜,另有开发工具、安全、前端、基础设施、设计、数据榜。数据来自 GitHub API,每天更新两次。", + "description": "按每天新增 star 数(涨星速度)而不是总 star 数给 GitHub 仓库排名。AI 为主榜,另有开发工具、安全、前端、基础设施、设计、数据榜。数据来自 GitHub API,每天更新。", "url": "https://jammyfu.github.io/github-risers/zh-Hans/", "sameAs": "https://github.com/jammyfu/github-risers", "license": "https://opensource.org/license/mit", @@ -116,7 +116,7 @@ "name": "多久更新一次?", "acceptedAnswer": { "@type": "Answer", - "text": "每天两次:北京时间 0 点(UTC+8)和美西洛杉矶时间 0 点。由 GitHub Actions 定时触发,可能晚几分钟开始。每次运行都会在仓库里单独存档。" + "text": "每天更新。每次运行都会在仓库里单独存档。" } }, { @@ -166,7 +166,7 @@

    GitHub 涨星榜(GitHub Risers)

    -

    按涨星速度(每天新增 star 数)而不是总 star 数,给 GitHub 热门仓库排名。AI 是主榜,开发工具、安全、前端、基础设施、设计、数据各有自己的榜。数字直接取自 GitHub API,每天更新两次。

    +

    按涨星速度(每天新增 star 数)而不是总 star 数,给 GitHub 热门仓库排名。AI 是主榜,开发工具、安全、前端、基础设施、设计、数据各有自己的榜。数字直接取自 GitHub API,每天更新。

    它是脚本生成的榜单:不是 GitHub 官方产品,不是人工精选清单,也不是买星或推广服务。

    查看最新榜单

    English

    @@ -174,14 +174,14 @@

    GitHub 涨星榜(GitHub Risers)

    关键事实

    - +
    是什么按 7、30、365 天的每日涨星速度排名,另有近 7 天新仓库榜
    分类AI(主榜)、开发工具、安全、Web / 前端、基础设施、设计 / 创意、数据
    更新时间每天北京时间 0 点和美西(洛杉矶)0 点各一次
    数据来源只用 GitHub REST API;取不到的数据标为缺失,不估算
    遥测无
    许可证MIT
    是什么按 7、30、365 天的每日涨星速度排名,另有近 7 天新仓库榜
    分类AI(主榜)、开发工具、安全、Web / 前端、基础设施、设计 / 创意、数据
    更新时间每天更新
    数据来源只用 GitHub REST API;取不到的数据标为缺失,不估算
    遥测无
    许可证MIT

    五种语言的榜单

    常见问题

    -
    GitHub 涨星榜是什么?

    一份按涨星速度而不是总 star 数排序的 GitHub 仓库榜单。AI 是主榜,开发工具、安全、前端、基础设施、设计、数据各有独立榜单。

    涨星速度怎么算?

    每日涨星 = 窗口内新增 star 数 ÷ 所用天数。新增数取自 GitHub API 的 stargazers/history,按运行日之前完整的 UTC 日累加。比窗口更新的仓库用自己的年龄做除数,并标为新仓库。

    多久更新一次?

    每天两次:北京时间 0 点(UTC+8)和美西洛杉矶时间 0 点。由 GitHub Actions 定时触发,可能晚几分钟开始。每次运行都会在仓库里单独存档。

    它和 GitHub Trending 一样吗?

    不一样。GitHub Trending 是 GitHub 自己的页面;涨星榜是独立的开源项目,规则公开,每次更新都有存档,与 GitHub 无任何关联。

    “最热新仓库”榜怎么选?

    近 7 天内创建、至少 100 star、非 fork、未归档的仓库,按 star 数 ÷ 仓库年龄(天)排序。

    能识别刷星吗?

    不能。它只如实报告 GitHub API 返回的数字。仅对疑似破解 / 盗版合集的新仓库用公开的规则标记,并排在所有正常仓库之后。

    用了生成式 AI 吗?

    榜单由普通脚本计算。新仓库描述的翻译可在一个可选步骤里用 AI 模型生成;数字从不由 AI 生成。

    怎么引用?

    使用仓库里的 CITATION.cff(GitHub 页面有“Cite this repository”按钮),并注明所用快照的日期。

    +
    GitHub 涨星榜是什么?

    一份按涨星速度而不是总 star 数排序的 GitHub 仓库榜单。AI 是主榜,开发工具、安全、前端、基础设施、设计、数据各有独立榜单。

    涨星速度怎么算?

    每日涨星 = 窗口内新增 star 数 ÷ 所用天数。新增数取自 GitHub API 的 stargazers/history,按运行日之前完整的 UTC 日累加。比窗口更新的仓库用自己的年龄做除数,并标为新仓库。

    多久更新一次?

    每天更新。每次运行都会在仓库里单独存档。

    它和 GitHub Trending 一样吗?

    不一样。GitHub Trending 是 GitHub 自己的页面;涨星榜是独立的开源项目,规则公开,每次更新都有存档,与 GitHub 无任何关联。

    “最热新仓库”榜怎么选?

    近 7 天内创建、至少 100 star、非 fork、未归档的仓库,按 star 数 ÷ 仓库年龄(天)排序。

    能识别刷星吗?

    不能。它只如实报告 GitHub API 返回的数字。仅对疑似破解 / 盗版合集的新仓库用公开的规则标记,并排在所有正常仓库之后。

    用了生成式 AI 吗?

    榜单由普通脚本计算。新仓库描述的翻译可在一个可选步骤里用 AI 模型生成;数字从不由 AI 生成。

    怎么引用?

    使用仓库里的 CITATION.cff(GitHub 页面有“Cite this repository”按钮),并注明所用快照的日期。

    文档