Repository navigation
Description
Context
The chapter onboarding checklist (inst/templates/chapter-setup.md) is almost entirely manual. Jinx currently only opens the setup/update issue and ccs the notify-teams; chapter_create_pr() exists but is not wired to anything.
This issue tracks automating the parts of the checklist that can be automated, sliced so that the work needing no new credentials ships first.
Slice 1 — no new secrets, no open decisions
Shipped so far: #137 (duplicate check), #138 (status command + nudges), #140 (prospective website entry), #141 (chapter details on the issue). CI gap found and fixed on the way: #139.
-
Duplicate + proximity check. Read
rladies.github.io/data/chapters/*.json, fuzzy-match the requested city, geocode to flag chapters within ~100 km, and post the result as the first comment on the onboarding issue with the nearby chapter's email ready to cc. This is checklist step 1 done automatically. -
Structured intake. (feat(chapter): carry chapter details on the onboarding issue itself #141 — machine-readable block on the issue; issue form in
new-chapters-onboardingstill to do) Issue form innew-chapters-onboarding(or an Airtable webhook in the shape ofairtable-invite.R) so the request arrives as parsed fields instead of prose. -
Onboarding state command.
/jinx chapter status <city>parses the issue checkboxes and reports what is done, what is blocked, and on whom. -
Staleness nudges. Reuse the
global-team-remind.Rpattern for the@rladies/emailand@rladies/meetup-prosections, which are where onboardings stall. -
Organiser Slack invite: track and prompt.
CallCorrected: that described something both duplicated and impossible.slack_invite_request()for the Community Slack immediately, and for the Organizers Slack once the Airtable organiser-form record appears.- Community Slack is already handled by the dedicated Airtable form + approval-card flow; jinx must not duplicate it.
- The invite cannot be automated.
admin.users.inviteis Enterprise-plan only (user token,admin.users:write) and unavailable to regular workspaces - which is why the community card ends with a human clicking Invite people.slack_invite_request()posts a request message and writes an audit flag; it does not invite anyone. - So the scope is tracking, not sending. Once the Airtable organiser-form record exists, jinx comments on the onboarding issue with a link to that Airtable record (not the email itself - organiser emails stay out of git history, which is why the issue metadata block omits them), a human sends the invite from Slack settings and confirms, and jinx ticks the checklist step and flips the Airtable audit flag. Same shape as the community card.
- Alternative considered and not chosen: email a shared Organisers-workspace invite link from
jinx@. Genuinely hands-off and needs no API, but a standing link lets anyone holding it into an organiser-only workspace. Viable with a short expiry if the team decides the tradeoff is worth it.
-
Website PR wired in. Call the existing
chapter_create_pr()withstatus = "prospective", requestrladies/leadershipreview, and letops-website-merge-pending.ymlmerge on approval.
Slice 2 — GitHub chapter team + repo
- Team
chapter-<city>under achaptersparent, members = organiser handles. - Opt-in repo (
/jinx chapter repo <city>) from achapter-templaterepo, team grantedmaintain. - Requires widening the App's org permissions (members: write, administration: write) — verify before building.
Slice 3 — Google Workspace chapter email
- Create
city@rladies.orgas a real Workspace mailbox via the Admin SDK Directory API (users.insert) (service account with domain-wide delegation; hand-rolled httr2 + JWT, or a worker endpoint). - Gated behind an explicit approval comment; temporary password never logged or posted.
- Post the address into the issue and add it to the chapter JSON in a follow-up commit.
Slice 4 — verification and follow-through
- Meetup: render the standard description with the exact name and URL slug into the issue, check the slug is free, then poll to confirm the group exists and the organiser holds co-organiser. (Group creation via the Pro GraphQL API is unproven — needs checking.)
- Confirmation email (draft or send) plus Airtable status update and issue closeout.
- Announce the new chapter via the existing
announce-*modules. - Nudge at 3 and 5 months if no event has been published, ahead of the 6-month deadline.
Decisions needed before slices 2 and 3
-
Chapter email: real Workspace mailbox, or Google Group/alias?Decided: a real mailbox. Groups are reserved for global teams. -
Who holds Workspace super-admin, and will they grant a service account domain-wide delegation forDecided. @drmowinckels has the access. Agreed design:admin.directory.user?- Chapter mailboxes live in a dedicated
/ChaptersOU. - DWD impersonates a purpose-made delegated admin (
jinx-provisioner@) holding a custom role scoped to/Chapters, carryingUsers > Createand nothing else. Note: Google grants Read and Update automatically alongside Create, and Update is what carries move/suspend/rename/reset-password/aliases — so "create only" is not achievable. Delete is separately excludable and stays off; retiring a mailbox remains manual. Only Organizational Units, Users, MDM, Chrome Management, User Security Management, shared device settings and Vault privileges can be OU-scoped at all — ticking anything else silently makes the role domain-wide. - The service account client ID is authorised for
admin.directory.userand nothing else. - The key lives as a Cloudflare Worker secret, behind one narrow "create chapter mailbox" endpoint using the existing bearer auth (
worker/src/api-auth.js), so it never reaches an Actions runner. Accepted trade: the Cloudflare account and the worker's auto-deploy-from-main pipeline are now part of the trust boundary for a domain-admin credential. - Set up and confirmed (2026-09-23): the role is scoped to
/Chapters. Groups privileges were tried and removed -Groupsis not in the OU-scopable set (Organizational Units, Users, MDM, Chrome Management, User Security Management, shared device settings, Vault), and including it removes the OU control from the role entirely rather than greying it out. Group management, if wanted later, needs its own role and its own service account: domain-wide delegation lets a key impersonate any user, so the impersonated admin's role bounds normal operation and the audit trail, while the client ID's authorised scope list is what bounds a stolen key. - Deliberate exception to the JS→R migration. The provisioning module stays in
worker/src/workspace.jsrather than being ported to R. R could do it (openssl::signature_create()signs RS256; ~40 lines of httr2), but porting it movesWORKSPACE_SA_PRIVATE_KEYonto a GitHub Actions runner, which is exactly what the Worker placement avoids. Code location follows key location. Recorded in the module header, the R client's roxygen,.github/AGENTS.md, and the README API table so a future migration pass does not sweep it up. Reversible:chapter_mailbox_create()is the only caller and its signature would not change if the key later moved into a protected GitHub environment. - Creation path is gated: local-part must match
^[a-z0-9-]+$, reserved names denylisted (admin,info,chapters,jinx,postmaster,abuse), a matching open onboarding issue must exist, creations capped per run, and an explicit human approval comment required. Temp password generated, never logged, change-on-first-login set. - Verified against current Google documentation: a custom role can be OU-scoped and still permit
users.insert(theUsers > Createprivilege explicitly grants Directory API permissions), subject to the scopable-privilege limitation above. Still to confirm at build time: thatadmin.directory.useris the narrowest OAuth scope allowingusers.insert, and current DWD scope-allowlist behaviour.
- Chapter mailboxes live in a dedicated
-
Chapter repo: default, opt-in, or not at all? Naming convention?
-
Chapter team: one per chapter under a
chaptersparent, or a singlechapter-organisersteam? -
Does jinx ever send email asDecided: jinx sends aschapters@rladies.org, or only prepare drafts?jinx@rladies.organd ccschapters@rladies.org. It never sends aschapters@, which means mail sending needs no domain-wide delegation - a per-account OAuth token forjinx@withgmail.sendis enough.
🤖 Generated with Claude Code
Activity
drmowinckels commentedon Sep 26, 2026
Slice 2 design, settled by the existing data
Decision recorded: one GitHub team per chapter, nested under the
chaptersparent, each with access to its own public repo.The naming convention already exists — it just isn't written down
Of the 122 teams under
chapters, 108 (89%) already use the Meetup urlname minus therladies-prefix. That is also the only field that disambiguates chapters sharing a city name:londonldnont/londonportlandpdx/portland-maineathensathens-ga/athens-groxfordoxford/oxford-ohioSeven city slugs are shared by more than one chapter, so city-alone cannot be the identity. Proposed: team slug = urlname minus
rladies-.One escape hatch is needed rather than a pure rule: Warsaw's urlname is
Spotkania-Entuzjastow-R-Warsaw-R-Users-Group-Meetup, which would make a dreadful team slug. The convention needs a manual override for cases like that.Display names
Currently inconsistent —
Warsaw,Mexico City, but also lowercasecapetown,nyc,philadelphia,jozi,sheffield. Proposed:City, Country, adding the r 8000 egion where a country has two of the same city, since "Portland, USA" is ambiguous and the chapter data carriesstate.region.The 14 teams that do not match
Eight point at a chapter whose urlname differs —
mexico-city/cdmx,lisbon/lisboa,portland/pdx,philadelphia/philly,louisville/louisville-ky,goettingen/gottingen,warsaw/(the long one),sheffield/(chapter has no urlname at all).Six match no chapter in the website data:
capetown,nyc,santiago,london-ontario,twin-cities,jozi. Retired, renamed, or missing from the data — each needs a human to say which.Renaming an existing team changes its URL and notification routing, so jinx should report these, not fix them. An audit command is the right deliverable; the renames are a human decision.
Also worth knowing: 122 teams against 232 active chapters, so team coverage has never been complete. Worth deciding whether a team is expected for every chapter or only on request.
What the App can actually do today
Checked rather than assumed —
jinx-familiar's installation permissions:So this splits:
chaptersparent.administrationpermission raised to write. That is an org-owner action on the App's settings.🤖 Generated with Claude Code
feat(chapter): name and audit the per-chapter GitHub teams
feat(chapter): name and audit chapter teams, and create their repos o…
feat(chapter): prompt the onboarding issue for the Organisers Slack i…
feat(chapter): prompt the onboarding issue for the Organisers Slack i…