2"""Report a "Sync with Forkgram" run on the GitHub issue labelled forkgram-sync.
4Runs in the report job of .github/workflows/sync-forkgram.yml, after the sync, compile and
5promote jobs. It reads their results and the sync outputs from the environment and:
7 - conflict, compile failure, promote failure, a sync error, or a multigram-next changed by
8 hand that the sync would have replaced: opens an issue, or comments on the open one, with
9 what happened and the local commands to fix it (the same event is reported once, however
10 often the schedule runs);
11 - multigram still behind with no new release: opens an issue only if none is open;
12 - promoted, or multigram in step with Forkgram: closes the open issue with a note (naming
13 any MultiGram commits the new multigram leaves out).
15Environment: SYNC_RESULT, COMPILE_RESULT, PROMOTE_RESULT (job results); SYNC_* (the sync
16job's outputs, e.g. SYNC_STATUS, SYNC_FORKGRAM_VERSION); PROMOTE_TAG; RUN_URL; GH_TOKEN
17and GH_REPO for the gh CLI. REPORT_GH names another gh binary (the self-test uses a fake).
28LABEL = 'forkgram-sync'
29GH = os.environ.get('REPORT_GH', 'gh')
30SHA = re.compile(r'^[0-9a-f]{40}$')
31VERSION = re.compile(r'^[0-9A-Za-z][0-9A-Za-z._+-]{0,39}$')
35 return os.environ.get(name, '').strip()
40 return value if SHA.match(value) else ''
44 return value[:10] if value else '?'
48 value = env('SYNC_FORKGRAM_VERSION')
49 return value if VERSION.match(value) else 'unknown'
53 """Untrusted text (commit subjects, file names, errors) as a Markdown code block."""
54 text = text.replace('`', "'").strip() or '(none)'
55 return '```text\n' + text + '\n```'
59 run = subprocess.run([GH, *args], capture_output=True, text=True)
60 if run.returncode != 0:
61 sys.stderr.write(run.stderr)
62 raise SystemExit(f'gh {args[0]} {args[1] if len(args) > 1 else ""} failed')
66def gh_body(*args, body):
67 with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as f:
71 return gh(*args, '--body-file', path)
77 issues = json.loads(gh('issue', 'list', '--label', LABEL, '--state', 'open',
78 '--json', 'number,title', '--limit', '20') or '[]')
79 return sorted(i['number'] for i in issues)
83 numbers = open_issues()
84 return numbers[0] if numbers else None
87def already_reported(number, key):
88 data = json.loads(gh('issue', 'view', str(number), '--json', 'body,comments'))
89 texts = [data.get('body') or ''] + [c.get('body') or '' for c in data.get('comments', [])]
90 return any(key in t for t in texts)
94 return f'<!-- forkgram-sync-key: {key} -->'
98 return f'Forkgram sync needs a hand (Forkgram {version()})'
103 return f'Run: {url}' if url.startswith('https://') else ''
107 return ('Then open **Actions > Sync with Forkgram > Run workflow**, tick **use_next** and run '
108 'it. It compiles `multigram-next` and, if that passes, moves `multigram` to it (the '
109 'old tip is kept as the tag `multigram-before-<version>`) and closes this issue.')
113 snap, prev = sha('SYNC_SNAPSHOT') or '<new snapshot>', sha('SYNC_PREVIOUS_SNAPSHOT') or '<old snapshot>'
114 commit = sha('SYNC_CONFLICT_COMMIT')
115 return f"""**Forkgram {version()}** is out and is now on `forkgram` (snapshot `{short(snap)}`), but the MultiGram stack does not apply to it. `multigram` is unchanged.
117MultiGram commit `{short(commit)}` ({env('SYNC_CONFLICT_POSITION') or '?'}) stops the rebase:
118{block(env('SYNC_CONFLICT_SUBJECT'))}
120{block(env('SYNC_CONFLICT_DETAILS') or env('SYNC_CONFLICT_FILES'))}
127git switch -C multigram-next origin/multigram
128git config rerere.enabled true # reuses your fixes if you redo this rebase in this clone
129git rebase --onto {snap} {prev}
130# fix the files git names, then:
132git rebase --continue # repeat until the rebase is done
133git push --force origin multigram-next
136To leave a MultiGram commit out instead (Forkgram now does the same thing, say), run `git rebase --skip` at that commit.
143 snap, cand = sha('SYNC_SNAPSHOT') or '<snapshot>', sha('SYNC_CANDIDATE')
144 return f"""The MultiGram stack applied cleanly to **Forkgram {version()}**, but the compile check failed. `multigram` is unchanged; the candidate is on `multigram-next` (`{short(cand)}`).
148If the log shows a runner or network hiccup, use **Re-run failed jobs** on the run page. Otherwise:
154git switch -C multigram-next origin/multigram-next
155# fix the build, then fold each fix into the MultiGram commit it belongs to,
156# one fixup per commit, naming that commit's files (PATCH.md 6.3 on the docs branch):
157git commit --fixup=<that commit> -- <its files>
158GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash {snap}
159git push --force origin multigram-next
167 cand = sha('SYNC_CANDIDATE')
168 return f"""The candidate for **Forkgram {version()}** compiled, but moving `multigram` to it failed, usually because someone pushed to `multigram` or `multigram-next` while the sync ran. `multigram` is unchanged; the candidate is still on `multigram-next` (`{short(cand)}`).
172Check that `multigram-next` holds everything `multigram` has now (rebase any new commits onto it and push it), then run **Actions > Sync with Forkgram** with **use_next**.
177 return f"""The sync stopped with an error. Nothing was pushed after the error; `multigram` is unchanged.
179{block(env('SYNC_ERROR') or env('SYNC_MESSAGE') or 'The sync job failed before it could say why; see the run log.')}
182Fix the cause (the message says what it is), then run **Actions > Sync with Forkgram** again.
186def body_next_edited():
187 nxt, tip, fc = sha('SYNC_EXISTING_NEXT'), sha('SYNC_FORKGRAM_TIP'), sha('SYNC_FORKGRAM_COMMIT')
188 if tip and tip != fc:
189 what = f"Forkgram's `dev` has moved on (`{short(tip)}`), but the sync did not take it"
191 what = 'The sync was asked to rebuild `multigram-next` (force), but did not'
192 return f"""{what}: `multigram-next` (`{short(nxt)}`) has commits the sync did not make, most likely a fix by hand, and the sync never replaces those. Nothing was pushed; `forkgram` and `multigram` are unchanged.
196- If `multigram-next` is ready, run **Actions > Sync with Forkgram > Run workflow** with **use_next**: it compiles `multigram-next` and, if that passes, moves `multigram` to it. The next scheduled sync then takes the new Forkgram commit.
197- If you do not need it any more, delete it (`git push origin --delete multigram-next`); the next sync rebuilds it from `multigram`.
202 return f"""`forkgram` has **Forkgram {version()}** (snapshot `{short(sha('SYNC_SNAPSHOT'))}`), but `multigram` is still based on the older snapshot `{short(sha('SYNC_MULTIGRAM_BASE'))}`: an earlier sync could not move it and no issue is open about it.
206Run **Actions > Sync with Forkgram** with **force** to try again; if the stack still does not apply, this issue gets the details and the commands to fix it.
210def report(key, body):
211 number = open_issue()
212 text = marker(key) + '\n' + body
214 gh('label', 'create', LABEL, '--color', 'D93F0B', '--force',
215 '--description', 'The automatic sync with Forkgram needs a hand')
216 print(gh_body('issue', 'create', '--title', title(), '--label', LABEL, body=text).strip())
218 if key and already_reported(number, marker(key)):
219 print(f'issue #{number} already reports this; nothing to add')
221 gh('issue', 'edit', str(number), '--title', title())
222 gh_body('issue', 'comment', str(number), body=text)
223 print(f'commented on issue #{number}')
227 for number in open_issues():
228 gh('issue', 'close', str(number), '--reason', 'completed', '--comment', note)
229 print(f'closed issue #{number}')
233 results = [env('SYNC_RESULT'), env('COMPILE_RESULT'), env('PROMOTE_RESULT')]
234 status = env('SYNC_STATUS')
235 print(f'sync={results[0]} compile={results[1]} promote={results[2]} status={status or "-"}')
236 if 'cancelled' in results:
237 print('a job was cancelled; nothing to report')
239 fc = sha('SYNC_FORKGRAM_COMMIT')
240 cand = sha('SYNC_CANDIDATE')
241 if status == 'conflict':
242 report(f'conflict {fc} {sha("SYNC_CONFLICT_COMMIT")}', body_conflict())
243 elif results[0] != 'success':
244 err = env('SYNC_ERROR') or 'unknown'
245 report('error ' + hashlib.sha1(err.encode()).hexdigest()[:12], body_error())
246 elif status == 'next-edited':
247 report(f'next-edited {sha("SYNC_EXISTING_NEXT")} {sha("SYNC_FORKGRAM_TIP")}', body_next_edited())
248 elif status == 'candidate' and results[1] == 'failure':
249 report(f'compile {cand}', body_compile())
250 elif status == 'candidate' and results[1] == 'success' and results[2] == 'failure':
251 report(f'promote {cand}', body_promote())
252 elif status == 'candidate' and results[2] == 'success':
253 tag = env('PROMOTE_TAG')
254 moved = f'the old tip is tagged `{tag}`' if re.match(r'^[0-9A-Za-z._-]+$', tag) else 'it already was the candidate'
255 note = f'Done: `multigram` now carries Forkgram {version()} (`{short(cand)}`); {moved}. {run_line()}'.strip()
256 if env('SYNC_DROPPED'):
257 note += ('\n\nThese MultiGram commits are not on the new `multigram` (Forkgram already has '
258 'them, or `multigram-next` left them out with allow_drop):\n' + block(env('SYNC_DROPPED')))
260 elif status == 'behind':
261 if open_issue() is None:
262 report('', body_behind())
264 print('multigram is behind and an issue is already open; nothing to add')
265 elif status == 'up-to-date':
266 if open_issue() is not None:
267 close(f'`multigram` is in step with Forkgram {version()} again. {run_line()}'.strip())
269 print('nothing to report')
272if __name__ == '__main__':
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list