From 1f3e4b292be50ea6db4c189a8f1381cf3be40523 Mon Sep 17 00:00:00 2001 From: Sean Dolbec Date: Sat, 31 Oct 2026 13:30:43 -0400 Subject: [PATCH] Upstream watch: store 12-character digests so the dashboard fits the issue body Since 2026-10-28, 23 of the last 30 scheduled runs failed with 'Dashboard exceeds GitHub issue size': 510 tracked sources (434 discovered branch heads) with 40-character SHAs, which barely compress, pushed issue #51 past 65,536 characters, so no new upstream changes were recorded. change_marker now keeps the first 12 characters of sha, base_sha and sha256. Stored full-length values shorten to the same prefix, so the first run after this reports no false changes; legacy pending entries go through the same marker. The live dashboard state migrates from 62.9k to 38.9k characters, and a test covers 520 sources all pending. --- mseries/local-ai/github-watch.py | 15 ++++++++++--- mseries/local-ai/test_github_watch.py | 32 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/mseries/local-ai/github-watch.py b/mseries/local-ai/github-watch.py index a671444d8..7ac57c9f7 100644 --- a/mseries/local-ai/github-watch.py +++ b/mseries/local-ai/github-watch.py @@ -13,6 +13,8 @@ HERE = Path(__file__).resolve().parent MARKER = '' TITLE = 'Upstream watch: changes awaiting review' +DIGEST_FIELDS = ('sha', 'base_sha', 'sha256') +DIGEST_CHARS = 12 def api(path, method='GET', data=None): @@ -35,8 +37,8 @@ def reconcile(previous, observations, errors, acknowledged=False): if not acknowledged: for key, change in previous.get('pending', {}).items(): # Migrate legacy {from, to} entries without losing the first value. - pending[key] = (change['from'] if isinstance(change, dict) - and 'from' in change and 'to' in change else change) + pending[key] = change_marker(change['from'] if isinstance(change, dict) + and 'from' in change and 'to' in change else change) for key, current in observations.items(): current = change_marker(current) old = snapshots.get(key) @@ -51,7 +53,14 @@ def change_marker(value): if not isinstance(value, dict): return value fields = ('sha', 'base_sha', 'branch', 'release', 'sha256', 'count', 'latest', 'pushed', 'ahead', 'behind', 'status') - return {k: value[k] for k in fields if k in value} + marker = {k: value[k] for k in fields if k in value} + # Hex digests barely compress, and 500+ tracked branch heads at 40 characters overflowed the + # 65,536-character issue body. Twelve characters still detect any move of the same ref, and + # stored full-length values shorten to the same prefix, so migration reports no false changes. + for k in DIGEST_FIELDS: + if isinstance(marker.get(k), str): + marker[k] = marker[k][:DIGEST_CHARS] + return marker def encode_state(state): diff --git a/mseries/local-ai/test_github_watch.py b/mseries/local-ai/test_github_watch.py index d3894b6ba..290af26b0 100644 --- a/mseries/local-ai/test_github_watch.py +++ b/mseries/local-ai/test_github_watch.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """State migration and bounded dashboard rendering for github-watch.py.""" +import hashlib import importlib.util from pathlib import Path import unittest @@ -13,6 +14,10 @@ people_spec.loader.exec_module(people) +def sha1(text): + return hashlib.sha1(text.encode()).hexdigest() + + class GitHubWatchState(unittest.TestCase): def test_legacy_pending_change_migrates_without_losing_latest_revision(self): key = 'branch:example/project:main' @@ -137,6 +142,33 @@ def test_large_legacy_dashboard_fits_the_issue_body_limit_after_migration(self): self.assertEqual(len(state['pending']), 173) self.assertLess(len(body.encode()), 65000) + def test_full_length_digests_shorten_without_reporting_a_change(self): + key = 'branch:example/project:main' + full, base = 'a1b2c3d4e5f6' + '0' * 28, 'f' * 40 + previous = {'snapshots': {key: {'sha': full, 'base_sha': base}}, + 'pending': {'heads:example/project': {'from': {'sha256': 'e' * 64}, 'to': {'sha256': 'd' * 64}}}} + + state = watch.reconcile(previous, {key: {'sha': full, 'base_sha': base}}, []) + + self.assertNotIn(key, state['pending']) + self.assertEqual(state['snapshots'][key], {'sha': 'a1b2c3d4e5f6', 'base_sha': 'ffffffffffff'}) + self.assertEqual(state['pending']['heads:example/project'], {'sha256': 'eeeeeeeeeeee'}) + + def test_every_source_pending_fits_with_520_full_length_heads(self): + # 2026-10-31: 510 tracked sources with 40-character SHAs overflowed the issue body. + observations, snapshots = {}, {} + for index in range(520): + key = f'branch:example/repository-{index % 24:02d}:feature/long-branch-name-{index:04d}' + snapshots[key] = {'branch': key.rsplit(':', 1)[1], 'sha': sha1(f'old-{index}'), + 'base_sha': sha1(f'base-{index}'), 'ahead': index, 'behind': index % 9, + 'status': 'diverged'} + observations[key] = dict(snapshots[key], sha=sha1(f'new-{index}')) + + state = watch.reconcile({'snapshots': snapshots}, observations, []) + + self.assertEqual(len(state['pending']), 520) + self.assertLess(len(watch.render(state).encode()), 65000) + class PeopleWatchRetry(unittest.TestCase): class Response: