import json, re, subprocess, sys, time, os Q = json.load(open(sys.argv[1])); LOG = sys.argv[2] if len(sys.argv) > 2 else 'final_run.log' done = set(re.findall(r'(issue|pr)#(\d+) (\w+)(?: \S+)? ok', open(LOG).read())) if os.path.exists(LOG) else set() Q = [q for q in Q if (q['kind'], str(q['n']), q['op']) not in done] N, t0, fails = len(Q), time.time(), [] log = open(LOG, 'a') def p(s): log.write(s + '\n'); log.flush() def already_posted(q): try: r = subprocess.run(['gh', q['kind'], 'view', str(q['n']), '-R', 'huggingface/transformers', '--json', 'comments,state', '--jq', '[.state, (.comments[-1].author.login // ""), (.comments[-1].body // "")]'], capture_output=True, text=True, timeout=60) st, who, body = json.loads(r.stdout) return who == 'ArthurZucker' and body.strip() == q['msg'].strip(), st except Exception: return False, None def edit_ours(q): qq = '{repository(owner:"huggingface",name:"transformers"){issueOrPullRequest(number:%d){... on Issue{comments(last:30){nodes{databaseId author{login} createdAt}}} ... on PullRequest{comments(last:30){nodes{databaseId author{login} createdAt}}}}}}' % q['n'] r = subprocess.run(['gh', 'api', 'graphql', '-f', 'query=' + qq], capture_output=True, text=True, timeout=60) nodes = json.loads(r.stdout)['data']['repository']['issueOrPullRequest']['comments']['nodes'] ours = [x for x in nodes if (x['author'] or {}).get('login') == 'ArthurZucker' and x['createdAt'] >= '2026-10-20'] if not ours: return subprocess.CompletedProcess([], 1, '', 'no close comment from 2026-10-20+ to edit') return subprocess.run(['gh', 'api', '-X', 'PATCH', f"repos/huggingface/transformers/issues/comments/{ours[-1]['databaseId']}", '-f', 'body=' + q['msg']], capture_output=True, text=True, timeout=60) for i, q in enumerate(Q, 1): if q['op'] == 'addrev': try: r = subprocess.run(['gh', 'pr', 'edit', str(q['n']), '-R', 'huggingface/transformers', '--add-reviewer', q['reviewer']], capture_output=True, text=True, timeout=60) except Exception as e: r = subprocess.CompletedProcess([], 1, '', str(e)) time.sleep(2); el = time.time() - t0 if r.returncode == 0: p(f'[{i}/{N}] {q["kind"]}#{q["n"]} addrev {q["reviewer"]} ok | elapsed {el:.0f}s') else: fails.append((q['kind'], q['n'], 'addrev: ' + (r.stderr or r.stdout)[:200])); p(f'[{i}/{N}] {q["kind"]}#{q["n"]} FAIL addrev {q["reviewer"]}: {(r.stderr or r.stdout)[:200]}') continue if q['op'] == 'edit': try: r = edit_ours(q) except Exception as e: r = subprocess.CompletedProcess([], 1, '', str(e)) time.sleep(4); el = time.time() - t0 if r.returncode == 0: p(f'[{i}/{N}] {q["kind"]}#{q["n"]} edit ok | elapsed {el:.0f}s') else: fails.append((q['kind'], q['n'], (r.stderr or r.stdout)[:200])); p(f'[{i}/{N}] {q["kind"]}#{q["n"]} FAIL: {(r.stderr or r.stdout)[:200]}') continue posted, st = already_posted(q) if posted and (q['op'] == 'comment' or st != 'OPEN'): p(f'[{i}/{N}] {q["kind"]}#{q["n"]} {q["op"]} ok (already posted) | elapsed {time.time()-t0:.0f}s'); continue cmd = ['gh', q['kind'], q['op'], str(q['n']), '-R', 'huggingface/transformers'] if q['op'] == 'close': cmd += (['-r', q['reason']] if q['kind'] == 'issue' else []) + ([] if posted else ['-c', q['msg']]) # comment already there → close only else: cmd += ['-b', q['msg']] for attempt in range(1000): try: r = subprocess.run(cmd, capture_output=True, text=True, timeout=120); out = r.stderr + r.stdout except subprocess.TimeoutExpired: p(f'[{i}/{N}] {q["kind"]}#{q["n"]} timeout, retrying in 30s'); time.sleep(30) posted, st = already_posted(q) # the timed-out call may have gone through if posted and (q['op'] == 'comment' or st != 'OPEN'): r = subprocess.CompletedProcess(cmd, 0, '', ''); break if posted and '-c' in cmd: cmd = cmd[:cmd.index('-c')] continue if r.returncode == 0: break if 'too quickly' in out or 'rate limit' in out.lower(): p(f'[{i}/{N}] {q["kind"]}#{q["n"]} rate-limited, sleeping 300s'); time.sleep(300); continue if ('Something went wrong' in out or 'timed out' in out or 'connection' in out.lower()) and attempt < 20: # transient network / GitHub 5xx time.sleep(30); continue break time.sleep(4) # ponytail: fixed spacing under GitHub's content-creation limit el = time.time() - t0 if r.returncode == 0: p(f'[{i}/{N}] {q["kind"]}#{q["n"]} {q["op"]} ok | elapsed {el:.0f}s | eta ~{el/i*(N-i):.0f}s') else: err = out.strip().replace('\n', ' ')[:200]; fails.append((q['kind'], q['n'], err)); p(f'[{i}/{N}] {q["kind"]}#{q["n"]} FAIL: {err}') p(f'done {sys.argv[1]} ok={N-len(fails)} fails={len(fails)}') json.dump(fails, open(sys.argv[1].replace('.json', '_fails.json'), 'w'))