8000 Automate the chapter onboarding checklist · Issue #136 · rladies/jinx · GitHub
[go: up one dir, main page]

Skip to content

Automate the chapter onboarding checklist #136

Open

Description

@drmowinckels

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-onboarding still to do) Issue form in new-chapters-onboarding (or an Airtable webhook in the shape of airtable-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.R pattern for the @rladies/email and @rladies/meetup-pro sections, which are where onboardings stall.

  • Organiser Slack invite: track and prompt. Call slack_invite_request() for the Community Slack immediately, and for the Organizers Slack once the Airtable organiser-form record appears. Corrected: that described something both duplicated and impossible.

    • 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.invite is 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() with status = "prospective", request rladies/leadership review, and let ops-website-merge-pending.yml merge on approval.

Slice 2 — GitHub chapter team + repo

  • Team chapter-<city> under a chapters parent, members = organiser handles.
  • Opt-in repo (/jinx chapter repo <city>) from a chapter-template repo, team granted maintain.
  • Requires widening the App's org permissions (members: write, administration: write) — verify before building.

Slice 3 — Google Workspace chapter email

  • Create city@rladies.org as 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

  1. Chapter email: real Workspace mailbox, or Google Group/alias? Decided: a real mailbox. Groups are reserved for global teams.

  2. Who holds Workspace super-admin, and will they grant a service account domain-wide delegation for admin.directory.user? Decided. @drmowinckels has the access. Agreed design:

    • Chapter mailboxes live in a dedicated /Chapters OU.
    • DWD impersonates a purpose-made delegated admin (jinx-provisioner@) holding a custom role scoped to /Chapters, carrying Users > Create and 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.user and 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 - Groups is 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.js rather than being ported to R. R could do it (openssl::signature_create() signs RS256; ~40 lines of httr2), but porting it moves WORKSPACE_SA_PRIVATE_KEY onto 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 (the Users > Create privilege explicitly grants Directory API permissions), subject to the scopable-privilege limitation above. Still to confirm at build time: that admin.directory.user is the narrowest OAuth scope allowing users.insert, and current DWD scope-allowlist behaviour.
  3. Chapter repo: default, opt-in, or not at all? Naming convention?

  4. Chapter team: one per chapter under a chapters parent, or a single chapter-organisers team?

  5. Does jinx ever send email as chapters@rladies.org, or only prepare drafts? Decided: jinx sends as jinx@rladies.org and ccs chapters@rladies.org. It never sends as chapters@, which means mail sending needs no domain-wide delegation - a per-account OAuth token for jinx@ with gmail.send is enough.

🤖 Generated with Claude Code

Activity

  1. drmowinckels commented on Sep 26, 2026

    @drmowinckels
    MemberAuthor

    Slice 2 design, settled by the existing data

    Decision recorded: one GitHub team per chapter, nested under the chapters parent, 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 the rladies- prefix. That is also the only field that disambiguates chapters sharing a city name:

    city slug chapters urlname short
    london London, Canada / London, United Kingdom ldnont / london
    portland Portland, USA / Portland, USA pdx / portland-maine
    athens Athens, USA / Athens, Greece athens-ga / athens-gr
    oxford Oxford, UK / Oxford, USA oxford / oxford-ohio

    Seven 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 lowercase capetown, 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 carries state.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:

    members: write                     -> team creation works
    organization_administration: read  -> repo creation does NOT
    

    So this splits:

    • 2a — buildable now: naming helpers, the audit command above, and team creation under the chapters parent.
    • 2b — blocked: creating the chapter repo and granting the team access needs the App's administration permission raised to write. That is an org-owner action on the App's settings.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0