Skip to content

Commit 056132c

Browse files
committed
fix: stop the nightly sync from committing timestamp-only diffs
The job committed every night even when upstream had not moved: synced_at in UPSTREAM.json was rewritten on every run, and build_index.py stamped generated_at from the current clock, so the staged diff was never empty and the git diff --quiet guard never fired. sync_upstream.sh now leaves UPSTREAM.json alone when the fetched upstream commit matches the recorded one, and generated_at is derived from synced_at instead of now(). A night with no upstream activity produces a genuinely empty diff. Adds a regression test and documents the reasoning in WORKFLOW.md. Also pins the one-time correction to data/tonies.json.
1 parent 0298af7 commit 056132c

5 files changed

Lines changed: 82 additions & 13 deletions

File tree

data/tonies.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/WORKFLOW.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ runs on a schedule (02:27 UTC nightly) and can also be triggered manually:
7676
`github-actions[bot]` with a message naming the upstream short SHA and
7777
file count, and pushes directly to the branch the workflow runs on.
7878

79+
"Nothing changed" is meant literally, which takes some care with timestamps:
80+
81+
* `sync_upstream.sh` leaves `data/UPSTREAM.json` untouched when the upstream
82+
commit it just fetched matches the one already recorded. Rewriting
83+
`synced_at` on every run would make the file differ every night even when
84+
the mirror is identical.
85+
* `build_index.py` takes `generated_at` from that `synced_at` rather than
86+
from the current clock, so `data/tonies.json` and `TONIES.md` inherit the
87+
same property.
88+
89+
Together this means a night with no upstream activity produces a genuinely
90+
empty diff and therefore no commit. `tests/test_build_index.py` guards the
91+
second half of this.
92+
7993
Triggering it manually (e.g. from the Actions tab, "Run workflow") accepts a
8094
`dry_run` input: when true, the sync and index rebuild still run, but the
8195
commit step only prints what it *would* commit (`git diff --cached --stat`)

scripts/build_index.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,36 @@ def collect_entries(tonies_dir: Path) -> list[dict]:
7474
return entries
7575

7676

77-
def load_upstream_commit() -> str:
77+
def load_upstream() -> dict:
78+
"""Read data/UPSTREAM.json, tolerating a missing or corrupt file."""
7879
if not UPSTREAM_JSON.exists():
79-
return "unknown"
80+
return {}
8081
try:
81-
data = json.loads(UPSTREAM_JSON.read_text(encoding="utf-8"))
82+
return json.loads(UPSTREAM_JSON.read_text(encoding="utf-8"))
8283
except (json.JSONDecodeError, OSError):
83-
return "unknown"
84-
commit = data.get("commit", "unknown")
84+
return {}
85+
86+
87+
def load_upstream_commit() -> str:
88+
commit = load_upstream().get("commit", "unknown")
8589
return commit[:7] if commit != "unknown" else commit
8690

8791

92+
def load_synced_at() -> str:
93+
"""Timestamp of the last upstream sync.
94+
95+
Deliberately not "now": the generated files must be byte-identical when
96+
the upstream mirror has not moved, otherwise the nightly job commits a
97+
timestamp-only diff every single night.
98+
"""
99+
return load_upstream().get(
100+
"synced_at", datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
101+
)
102+
103+
88104
def build_tonies_json_payload(entries: list[dict], upstream_commit: str) -> dict:
89105
return {
90-
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
106+
"generated_at": load_synced_at(),
91107
"upstream_commit": upstream_commit,
92108
"count": len(entries),
93109
"tonies": entries,
@@ -187,12 +203,7 @@ def render_tonies_md(
187203

188204
def write_tonies_md(entries: list[dict]) -> None:
189205
upstream_commit = load_upstream_commit()
190-
synced_at = "unknown"
191-
if UPSTREAM_JSON.exists():
192-
try:
193-
synced_at = json.loads(UPSTREAM_JSON.read_text(encoding="utf-8")).get("synced_at", "unknown")
194-
except (json.JSONDecodeError, OSError):
195-
pass
206+
synced_at = load_upstream().get("synced_at", "unknown")
196207
TONIES_MD.write_text(render_tonies_md(entries, upstream_commit, synced_at), encoding="utf-8")
197208

198209

scripts/sync_upstream.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ AFTER_COUNT="$(find "$DEST_DIR" -name '*.nfc' | wc -l | tr -d ' ')"
5656

5757
python3 - "$UPSTREAM_JSON" "$UPSTREAM_URL" "$UPSTREAM_BRANCH" "$COMMIT_SHA" "$COMMIT_DATE" "$SYNCED_AT" "$AFTER_COUNT" <<'PYEOF'
5858
import json
59+
import os
5960
import sys
6061
6162
out_path, repository, branch, commit, commit_date, synced_at, file_count = sys.argv[1:8]
@@ -69,6 +70,24 @@ data = {
6970
"file_count": int(file_count),
7071
}
7172
73+
# Only rewrite the file when the mirror actually moved. "synced_at" changes on
74+
# every run by definition, so writing it unconditionally would produce a
75+
# timestamp-only diff -- and therefore a pointless commit -- every night.
76+
# Everything else in the record is derived from the upstream commit, so an
77+
# unchanged SHA means an unchanged mirror.
78+
if os.path.exists(out_path):
79+
try:
80+
with open(out_path, encoding="utf-8") as f:
81+
previous = json.load(f)
82+
except (json.JSONDecodeError, OSError):
83+
previous = None
84+
85+
if previous is not None:
86+
comparable = {k: v for k, v in data.items() if k != "synced_at"}
87+
if all(previous.get(k) == v for k, v in comparable.items()):
88+
print("Upstream unchanged, keeping existing UPSTREAM.json", file=sys.stderr)
89+
sys.exit(0)
90+
7291
with open(out_path, "w", encoding="utf-8") as f:
7392
json.dump(data, f, indent=2, ensure_ascii=False)
7493
f.write("\n")

tests/test_build_index.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,31 @@ def test_json_payload_shape(self):
8686
self.assertIn("generated_at", payload)
8787
json.dumps(payload) # must be JSON-serializable
8888

89+
def test_generated_at_tracks_the_sync_not_the_clock(self):
90+
"""Two builds must be byte-identical while the mirror has not moved.
91+
92+
Regression guard: deriving generated_at from datetime.now() made the
93+
nightly job commit a timestamp-only diff every single night.
94+
"""
95+
entries = build_index.collect_entries(self.tonies_dir)
96+
upstream = {
97+
"commit": "e6f8fd419bed396be9575930412da9a0e2d7a6cc",
98+
"synced_at": "2026-08-02T17:17:01Z",
99+
}
100+
upstream_json = Path(self.tmp) / "UPSTREAM.json"
101+
upstream_json.write_text(json.dumps(upstream), encoding="utf-8")
102+
103+
original = build_index.UPSTREAM_JSON
104+
build_index.UPSTREAM_JSON = upstream_json
105+
try:
106+
first = build_index.build_tonies_json_payload(entries, "e6f8fd4")
107+
second = build_index.build_tonies_json_payload(entries, "e6f8fd4")
108+
finally:
109+
build_index.UPSTREAM_JSON = original
110+
111+
self.assertEqual(first["generated_at"], upstream["synced_at"])
112+
self.assertEqual(json.dumps(first), json.dumps(second))
113+
89114
def test_duplicate_uid_detection(self):
90115
entries = build_index.collect_entries(self.tonies_dir)
91116
dupes = build_index.find_duplicate_uids(entries)

0 commit comments

Comments
 (0)