Conversation
One message each weekday morning listing the open PRs across session-foundation's own repositories whose author is not a maintainer and which have moved in the last three days. A single search fetches every open PR in the org and the window is applied to the result, so the header can carry the total contributor backlog for the cost of one query. Forks and archived repos are excluded by checking results against the org's repository list rather than by name, and bot accounts on GitHub's own account type. Weekdays means Monday has to cover the weekend, so the window is 72h and overlaps itself by two days on every run. A state file absorbs the overlap: 🟢 is a PR never reported, ✏️ one that has moved since it was, and a PR that has not moved is left out. Only what Discord accepted is recorded, so a run that fails partway re-reports that message's PRs rather than losing them, and every way of failing to read the file treats the window as new — noisy once, never wrong. Dedup is keyed on updated_at, which moves on any change at all, so an edit touching several PRs at once resurfaces all of them. The accurate alternative needs a request per PR that moved; this is the cheaper half of that trade. Maintainers are a hand-written list. Org membership covers six accounts, two of which are not in the review loop, and push access is held by a dozen more as outside collaborators — several of them contractors whose PRs are the point of the digest. Both signals would get it wrong in both directions. Runs on the box that already hosts the Zendesk digest, under its own user, env file and venv — the venv because the two jobs pin requests differently.
Bilb
force-pushed
the
feat/contributor-pr-digest
branch
from
September 24, 2026 02:08
5131343 to
ae60e0d
Compare
A block over the text budget was posted whole and rejected, and since nothing in a rejected message is recorded, the same block was rebuilt on every run until the PRs aged out of the window. Blocks are now split under a repeated heading, each sized to fit beside the header.
…s in the docs Exclusion is by repository property, so nothing private is named anywhere in the repo. The token guidance drops the option of a `repo` scope with it: the digest posts to Discord, and nothing about a private repository belongs there.
…eads Forks, archived and private repositories are always left out; there was no run that wanted them back in.
The envelope's `result` carries the reason whichever way the CLI exits, and the exit-1 path already reads it. The exit-0 path reported only the subtype and the status code, which does not say what went wrong.
…ers into shared/ The pull request digest was about to carry its own copy of each; crowdin's report already does. One copy, imported by every script out of the same clone. triage.py keeps get_env, request_with_retry, clip and post_to_discord in its namespace: note_reply, resolve_reviews and the alert reach them there, and the tests patch them there.
The digest carried its own copy of the retry loop, the dedup state file, the Components V2 constants, the chunker and the webhook posting. It now imports them from shared/, and keeps only what is its own: the record it stores per PR, its per-message block cap, and the rendering. The tests that covered the copies move with the code; what stays here covers the wrappers and the rendering.
…imeout RFC 9110 allows Retry-After in either form. Crowdin's report already parsed the date form, so the shared loop has to before it can replace that copy; a date already gone reads as no header, since time.sleep() rejects a negative. The timeout was fixed at 30s; Crowdin's scan runs on 60.
…k code The third copy of the retry loop and the Discord posting. What stays is Crowdin's own contract: ten attempts at a 60s timeout, a 4xx that raises so callers can read the body unchecked, and the plain-text warning posted before the run exits on a rejected embed.
Bilb
force-pushed
the
feat/contributor-pr-digest
branch
from
September 25, 2026 02:31
475dac1 to
36702d3
Compare
This was referenced Sep 25, 2026
Bilb
commented
Sep 26, 2026
Comment on lines
+669
to
+670
| 🟢 [#1958](…) @KyWB · 12h · 💬1 · Fix issue #563 | ||
| ✏️ [#1904](…) @scrense-hash · 3h · 💬2 · feat: add SOCKS5 proxy support |
Collaborator
Author
There was a problem hiding this comment.
don't use real users here, or use Bilb
Collaborator
Author
|
Folded into #65, which now targets main and carries every commit from this branch. #65 replaces most of this PR's scaffolding (the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One Discord message each weekday morning listing the open PRs across session-foundation's own repositories whose author is not a maintainer and which have moved in the last three days — the ones nobody on the team has a reason to already know about.
🟢 is a PR the digest has never reported; ✏️ is one it has, which has moved since. A PR that hasn't moved is dropped from the message entirely while still counting in the backlog line.
Weekdays, and the state file
Mon..Fri 09:30 Australia/Melbourne, so Monday's run has to cover the weekend — hence a 72h window, which then overlaps itself by two days on every run.--stateabsorbs the overlap: it records which PRs reached Discord and each one'supdated_atat the time. Same mechanism aszendesk-digest, whose--window-hours 72 --state seen.jsonsolved this first — and now literally the same code, see below.Dedup is keyed on
updated_atalone. That is a deliberate trade:updated_atmoves on any change, so an edit touching several PRs at once resurfaces all of them — threesession-iosPRs in the org currently share anupdated_atto the second, from a label sweep. The accurate alternative (head SHA + comment counts) isn't in the search result and costs a request per PR that moved. Both halves are written down inactivity_keyand the README.Only what Discord accepted is recorded, so a run failing on its second message re-reports that message's PRs rather than losing them. Every way of failing to read the state file — missing, unreadable, wrong version — treats the window as new: noisy once, never wrong.
How it decides what to show
One search fetches every open PR in the org and the window is applied to the result, which buys the backlog line for the cost of a single query. Past GitHub's 1000-result ceiling it reports the counts as a floor rather than failing.
Maintainers are a hand-written list. Neither signal GitHub offers works: org membership covers six accounts, two of which aren't in the review loop; push access is held by a dozen more as outside collaborators, several of them contractors whose PRs are exactly what this is for. Both would get it wrong in both directions.
Bots are dropped on GitHub's own account type, so a renamed Dependabot stays out without an entry. Forks, archived and private repos are excluded by checking search results against the org's repository list rather than by name, so a repo created today is covered today. Private repositories are never reported whatever the token can see: the digest posts to Discord, and nothing about them belongs there.
shared/: one copy of the plumbingThe first draft of this digest carried its own copy of the retry loop, the Discord Components V2 posting, the dedup state file and
get_env, on the theory that a shared module would tie the two jobs' dependencies together. That theory was already false:deploy/alert.pyimportedtriageacross directories, so the github-prs venv had to satisfy triage's imports anyway — the standard library plusrequests. And the crowdin report carried a third copy of the retry and posting code.So the plumbing moves to a
shared/package at the repo root, imported the wayalert.pyalready did it (the script inserts the repo root onsys.path; every job runs out of the same clone, so there is no install step):shared.retry—request_with_retrywith backoff on 429/5xx,Retry-Afterin both forms RFC 9110 allows (seconds or HTTP-date), GitHub'sx-ratelimit-resetwhen there is noRetry-After, and atimeoutargument.shared.discord— the Components V2 constants, the chunker, a header-plus-entries message builder,delivered_idsfor what a partial post covered, thewith_componentswebhook fix-up andpost_to_discord, which returns the accepted count rather than raising so a caller can record what landed.shared.state— the dedup file, and aTrackerthat binds a digest's schema version, item key, activity field and retention once, so the new/changed/unchanged partition and the record building live in one place.shared.text—clip,squash,window_label.shared.env—get_env, now with adefaultfor settings that are optional with a fallback.shared.testing— the fakes every suite used to define for itself.What each script keeps is its own contract. The Zendesk digest keeps
requester_updated_atas its activity key and the header-coverage rule. This digest keeps the record it stores per PR, its per-message block cap and the rendering. The crowdin report keeps ten attempts at a 60s timeout, a 4xx that raises so callers can read the body unchecked, and the plain-text warning it posts before exiting on a rejected embed.note_reply,resolve_reviews,relayand the alert importshareddirectly instead of reaching the helpers as attributes of a 1900-line classifier.The rest of the way out of
triage.pynote_reply,resolve_reviewsand the alert reached the Zendesk fetches, the marker format and the CLI's name by importing the 1900-line classifier. Three modules take that over, and nothing outsidetriage.pyimports it any more:zendesk_triage/zendesk.py— the API session, search and its 1000-result ceiling, one ticket, its comments and users, the one PUT every write is, the comment markers, who the customer is on a channel integration, and what an imported store review looks like. The three comment fetcherstriage.pycarried differed only in page size, order and whether a failure exits or skips the ticket; those are parameters of onefetch_comments.zendesk_triage/claude_cli.py—run_json, andtry_run_jsonfor an enrichment that hands the failure back instead of exiting, in place of three copies of the catch-SystemExitidiom.note_reply --modelgoes throughresolve_api_modellike the triage's flag does, so an alias works for both.zendesk_triage/transcript.py— the English transcript: translate, render, write to the field, attach before the post.triage.pykeeps the queries, the taxonomy, the dedup, the review filter, the analysis and the rendering: 1091 lines, from 1913.undash_englishis pure text and moves toshared.text.download_translations_from_crowdin.pygoes throughshared.retrytoo. Its own loop retried 429 only, crashed on aRetry-Aftergiven as a date, and re-sent the bearer token per call with no session. It now uses one session for the API and a second, unauthenticated one for the export downloads, which are served from a signed URL on another host. Arguments parse inmainrather than at import, which is what let it get tests; the workflow's invocation is unchanged.One small fix on the way
claude_cli.run_json(claude_cli_jsonat the time) reports the CLI's reason when it exits 1 (#60), but the exit-0is_errorbranch still reported only the subtype and status code. It now appends the envelope'sresultas well.Deployment
The box that already runs the Zendesk digest, with its own user, environment file and venv — the venv because the two jobs pin
requestsdifferently, and a shared one would silently be whichever was installed last.StateDirectory=github-prscreates the state directory on first start.shared/ships with the clone and needs nothing installed.deploy/alert.pygains anALERT_DISCORD_WEBHOOK_URLoverride so this job's failures land in its own channel rather than the triage one.Testing
Six suites, each
python -m unittest discoverfrom its directory..github/workflows/tests.ymlruns them all on every pull request, one job per directory because two requirements files pinrequestsdifferently; nothing ran them before.sogs_moderationneedspython3-session-util, a deb built against the system interpreter, so its job installs it from deb.session.foundation and runs on that interpreter.sharedRetry-Afterforms and the reset epoch, chunking and message assembly, webhook posting and partial delivery, the state file and theTracker, text helpers,get_envzendesk_triagegithub_prscrowdindeploysogs_moderationVerified against the live org and a test webhook rather than only mocked:
A busy-day render split into 2 messages (3830 + 743 chars) on a repo boundary, confirming the chunker against Discord's real 4000-char Components V2 ceiling and that a plain incoming webhook accepts the container. After the move to
shared/, a read-only--dry-runagainst the org rendered the same shape through the shared builder, both for a quiet 3-day window and a 30-day one with repository blocks.