1313import datetime
1414import json
1515import os
16+ import re
1617import shutil
1718
1819import gi
@@ -94,7 +95,8 @@ def _fmt_date(created):
9495class CiPage :
9596 def __init__ (self , win ):
9697 self .win = win
97- self ._ia_link = None # derived archive.org details URL
98+ self ._ia_link = None # archive.org details URL of the newest item
99+ self ._ia_sha = None # source commit of the newest published ISO
98100 self ._pub_url = None # newest successful publish run's URL
99101 self ._status_busy = False # a dashboard refresh is in flight
100102 self ._status_pending = 0
@@ -271,6 +273,7 @@ def refresh(self):
271273 for row in (self .stable_row , self .pubstat_row , self .ia_row ):
272274 row .set_subtitle ("checking…" )
273275 self ._ia_link = None
276+ self ._ia_sha = None
274277 self ._pub_url = None
275278 self .ia_copy_btn .set_visible (False )
276279 self .ia_open_btn .set_visible (False )
@@ -347,83 +350,57 @@ def viewed(ok2, out2, err2):
347350 "--workflow" , "build-disk.yml" , "--event" , "workflow_dispatch" ,
348351 "--limit" , "1" , "--json" , "databaseId,createdAt" ], listed )
349352
353+ # Until 2026-07-11 this row RECONSTRUCTED the identifier from the last
354+ # SUCCESSFUL dispatch run (publish-job start date). Twice in a week that
355+ # showed days-old items, because IA publishes can complete even when the
356+ # run reports failure (2026-07-06 multipart-ETag false-fail, 2026-07-10
357+ # rationing verify-timeout): the last "successful" run was much older
358+ # than the last actually-published ISO. Ask the Archive itself instead:
359+ # the newest margine-live-iso-* item IS the truth, and its description
360+ # carries the source commit (written by the publish step), which also
361+ # gives "Changes since last publish" an honest base.
350362 def _q_ia (self ):
351- def listed (ok , out , err ):
352- runs = _runs (out ) if ok else []
353- if not runs :
363+ def searched (ok , out , _err ):
364+ ident = None
365+ if ok and out .strip ():
366+ try :
367+ docs = json .loads (out ).get ("response" , {}).get ("docs" , [])
368+ if docs :
369+ ident = docs [0 ].get ("identifier" )
370+ except ValueError :
371+ ident = None
372+ if not ident :
354373 self .ia_row .set_subtitle (
355- ("unavailable — " + _errline (err )) if not ok
356- else "no successful publish run yet" )
374+ "archive.org unreachable — retry with the refresh button" )
357375 self ._done_one ()
358376 return
359- self ._pub_url = runs [0 ].get ("url" ) or ""
377+ self ._ia_link = f"https://archive.org/details/{ ident } "
378+ self .ia_row .set_subtitle (_esc (ident ))
379+ self .ia_copy_btn .set_visible (True )
360380
361- def viewed (ok2 , out2 , _err2 ):
381+ def got_meta (ok2 , out2 , _err2 ):
362382 try :
363- data = None
364383 if ok2 and out2 .strip ():
365- try :
366- data = json .loads (out2 )
367- except ValueError :
368- data = None
369- ident = self ._derive_ia_identifier (data )
370- if ident :
371- self ._ia_link = f"https://archive.org/details/{ ident } "
372- self .ia_row .set_subtitle (_esc (ident ))
373- self .ia_copy_btn .set_visible (True )
374- else :
375- self .ia_row .set_subtitle (
376- "No Archive link for this run "
377- "(non-stable variant?) — open the run page" )
378- self .ia_open_btn .set_visible (bool (self ._pub_url ))
384+ desc = (json .loads (out2 ).get ("metadata" , {})
385+ or {}).get ("description" , "" )
386+ m = re .search (r"commit ([0-9a-f]{7,40})" , desc )
387+ if m :
388+ self ._ia_sha = m .group (1 )
389+ except ValueError :
390+ pass
379391 finally :
380392 self ._done_one ()
381393
382- core .gh (["run" , "view" , str (runs [0 ]["databaseId" ]),
383- "--repo" , core .GH_REPO , "--json" , "jobs" ], viewed )
384-
385- core .gh (["run" , "list" , "--repo" , core .GH_REPO ,
386- "--workflow" , "build-disk.yml" , "--event" , "workflow_dispatch" ,
387- "--status" , "success" , "--limit" , "1" ,
388- "--json" , "databaseId,url" ], listed )
394+ core .spawn_collect (
395+ ["curl" , "-s" , "--max-time" , "15" ,
396+ f"https://archive.org/metadata/{ ident } " ], got_meta )
389397
390- @staticmethod
391- def _derive_ia_identifier (data ):
392- """Reproduce the ia_upload step of .github/workflows/build-disk.yml:
393-
394- DATE_TAG="$(date -u +%Y%m%d)" # taken IN the publish_ia job
395- VSLUG="live" for stable, else the raw tag (e.g. "nvidia")
396- IDENTIFIER="margine-${VSLUG}-iso-${DATE_TAG}"
397- → https://archive.org/details/$IDENTIFIER
398-
399- What is reconstructable from run data:
400- * DATE_TAG — the 'Publish ISO' JOB's startedAt (UTC day), NOT the
401- run's createdAt: `gh run rerun --failed <id>` re-runs just
402- publish_ia days later and DATE_TAG is re-derived then.
403- * variant — workflow_dispatch inputs are not exposed by the runs
404- API, but install_gate's `if:` runs it exactly when image_tag is
405- ''/'stable', so gate success => stable => VSLUG "live". A skipped
406- gate means a non-stable tag whose VALUE is unrecoverable — do not
407- guess (return None → the run-page fallback button).
408- matrix.image is single-entry "margine" (gaming variant retired
409- 2026-06-06), so the identifier prefix is a constant.
410- """
411- gate_s , gate_c = _job (data , "Automated install gate" )
412- if gate_s != "completed" or gate_c != "success" :
413- return None
414- pub = None
415- for j in (data or {}).get ("jobs" , []):
416- if j .get ("name" , "" ).startswith ("Publish ISO" ):
417- pub = j
418- break
419- if pub is None or pub .get ("conclusion" ) != "success" :
420- return None
421- ts = core .iso_ts (pub .get ("startedAt" ) or "" )
422- if ts <= 0 :
423- return None
424- date_tag = datetime .datetime .fromtimestamp (
425- ts , datetime .timezone .utc ).strftime ("%Y%m%d" )
426- return f"margine-live-iso-{ date_tag } "
398+ core .spawn_collect (
399+ ["curl" , "-sg" , "--max-time" , "15" ,
400+ "https://archive.org/advancedsearch.php"
401+ "?q=identifier:margine-live-iso-*"
402+ "&fl[]=identifier&sort[]=identifier+desc&rows=1&output=json" ],
403+ searched )
427404
428405 def on_copy_ia (self , _btn ):
429406 if not self ._ia_link :
@@ -454,16 +431,7 @@ def on_open_pubstat_run(self, _btn):
454431 def on_changelog (self , _btn ):
455432 self .chlog_btn .set_sensitive (False )
456433
457- def listed (ok , out , err ):
458- runs = _runs (out ) if ok else []
459- if not runs or not runs [0 ].get ("headSha" ):
460- self .chlog_btn .set_sensitive (True )
461- self .win .toast (("gh failed: " + _errline (err )) if not ok else
462- "No successful publish run found — nothing to diff against" )
463- return
464- sha = runs [0 ]["headSha" ]
465- since = _fmt_date (runs [0 ].get ("createdAt" , "" ))
466-
434+ def diff_from (sha , since ):
467435 def got (ok2 , out2 , err2 ):
468436 self .chlog_btn .set_sensitive (True )
469437 if not ok2 :
@@ -478,6 +446,26 @@ def got(ok2, out2, err2):
478446 core .spawn_collect (["git" , "-C" , core .REPO_ROOT , "log" ,
479447 "--oneline" , f"{ sha } ..HEAD" ], got )
480448
449+ # Preferred base: the source commit of the newest PUBLISHED ISO,
450+ # read from the Archive item itself by _q_ia (truthful even when
451+ # the publish run reported failure but the upload landed, as on
452+ # 2026-07-06 and 2026-07-10). Identifier date doubles as "since".
453+ if self ._ia_sha and self ._ia_link :
454+ diff_from (self ._ia_sha , self ._ia_link .rsplit ("-" , 1 )[- 1 ])
455+ return
456+
457+ # Fallback (Archive metadata not loaded yet / unreachable): the
458+ # last successful dispatch run's head commit, the pre-2026-07-11
459+ # behavior.
460+ def listed (ok , out , err ):
461+ runs = _runs (out ) if ok else []
462+ if not runs or not runs [0 ].get ("headSha" ):
463+ self .chlog_btn .set_sensitive (True )
464+ self .win .toast (("gh failed: " + _errline (err )) if not ok else
465+ "No successful publish run found — nothing to diff against" )
466+ return
467+ diff_from (runs [0 ]["headSha" ], _fmt_date (runs [0 ].get ("createdAt" , "" )))
468+
481469 core .gh (["run" , "list" , "--repo" , core .GH_REPO ,
482470 "--workflow" , "build-disk.yml" , "--event" , "workflow_dispatch" ,
483471 "--status" , "success" , "--limit" , "1" ,
0 commit comments