|
| 1 | +# CLA Bot — Product & Behavior Spec |
| 2 | + |
| 3 | +This document is the authoritative behavior contract for the CLA Bot application. |
| 4 | +It covers product requirements, page routes, scenario catalog, webhook/PR flows, |
| 5 | +and test coverage expectations. |
| 6 | + |
| 7 | +## Product Requirements |
| 8 | + |
| 9 | +- If a contributor has signed a non-current CLA version, they must re-sign before being considered compliant. |
| 10 | +- Contributor compliance status is evaluated per org using the contributor's latest signed version for that org. |
| 11 | +- Admins can define org-scoped bypass lists for both GitHub users and GitHub Apps/system bots that should always receive a passing CLA check. |
| 12 | +- If a contributor has open pull requests and their signature becomes outdated after a CLA update, checks may need to be re-opened/re-evaluated and set to failing until re-signing is completed. |
| 13 | +- After a contributor signs/re-signs the latest CLA, the app schedules an async workflow that updates their open PR CLA checks to success and removes stale CLA prompt comments. |
| 14 | +- When an org is activated/deactivated, the app schedules an async workflow to re-check open PRs for that org so checks converge to the new enforcement mode. |
| 15 | +- GitHub is the user-management source of truth for the app. |
| 16 | +- The app has no local signup/password user-management system; DB user rows are GitHub-linked identity mirrors only. |
| 17 | +- Authentication/session management is stateless JWT-based (HTTP-only cookie + signed JWT with `jti`). |
| 18 | +- Users can only log in via GitHub OAuth. |
| 19 | +- Contributors can view and download every CLA version they have signed. |
| 20 | +- Admins can download both current and archived CLA versions for managed orgs. |
| 21 | + |
| 22 | +## Page Spec |
| 23 | + |
| 24 | +This section is the behavior contract for UI routes. |
| 25 | + |
| 26 | +| Route | Purpose | Signed-out behavior | Signed-in behavior | Key actions | |
| 27 | +| --- | --- | --- | --- | --- | |
| 28 | +| `/` | Marketing landing page | Public page | Same | CTA to `/auth/signin`, example CLA link to `/sign/fiveonefour` | |
| 29 | +| `/auth/signin` | Start sign-in flow | Shows GitHub sign-in CTA | Same | Sends user to `/api/auth/github?returnTo=...`; `returnTo` is sanitized to internal paths only | |
| 30 | +| `/dashboard` | Mode selector page | Public page | Same + session shown in header | Navigate to `/admin` or `/contributor` | |
| 31 | +| `/admin` | Org admin overview | Shows "Sign in required" card | Lists organizations user can administer; shows install CTA when none are authorized | Install app (`/api/github/install`), open org manage pages | |
| 32 | +| `/admin/[orgSlug]` | Org CLA management | If data unavailable, shows "Organization not found" UI | Shows org details, CLA version, signers, archives, bypass list, branch-protection reminder | Edit/save CLA text with live markdown preview modes (`Edit`, `Split`, `Preview`), activate/deactivate bot, copy signing link, inspect signers/archives, manage bypass users and app/bot slugs, download current/archived CLA text, share tab links via `?tab=cla\|signers\|archives\|bypass` | |
| 33 | +| `/contributor` | Contributor agreement dashboard | Shows "Sign in required" card | Lists signed CLA history grouped by org status | Re-sign prompts for outdated orgs, links to `/sign/[orgSlug]`, download previously signed CLA records | |
| 34 | +| `/sign/[orgSlug]` | CLA read/sign page | Shows sign-in required (or org not found) | Shows signed state, or sign/re-sign workflow | Requires scroll-to-bottom before sign button enables; handles inactive org warning | |
| 35 | +| `/terms` | Legal terms page | Public page | Same | Documents signing/enforcement terms and branch-protection requirement | |
| 36 | +| `/privacy` | Privacy policy page | Public page | Same | Documents collected data, retention, and rights workflow | |
| 37 | + |
| 38 | +## Scenario Catalog |
| 39 | + |
| 40 | +### 1) First login with the app |
| 41 | + |
| 42 | +- Users can only authenticate via GitHub OAuth. |
| 43 | +- OAuth flow validates `state` and redirects back to a sanitized internal `returnTo`. |
| 44 | +- Session is maintained with an HTTP-only cookie containing a JWT payload (`userId`, `githubUsername`, `role`, `jti`). |
| 45 | +- GitHub remains the source of truth for identity. The DB stores app-side profile/session linkage metadata, not standalone account management. |
| 46 | + |
| 47 | +### 2) User selects Admin |
| 48 | + |
| 49 | +- If signed out: user sees auth-required state and can start GitHub login. |
| 50 | +- If signed in and authorized on at least one installed account: user sees the account list and install button. |
| 51 | +- If signed in but authorized on zero installed accounts: user sees install CTA for GitHub App flow. |
| 52 | +- Newly installed accounts start with no CLA text. Maintainers must publish their own CLA before external contributors can sign. |
| 53 | +- Org manage tabs are URL-synced (`/admin/[orgSlug]?tab=...`) so state is shareable and works with browser back/forward navigation. |
| 54 | + |
| 55 | +### 3) User selects Contributor |
| 56 | + |
| 57 | +- User sees signed CLA records including org, version label/hash prefix, and signed timestamp. |
| 58 | +- User can view full signing history and download each signed record they own. |
| 59 | +- Org compliance status uses the latest signature per org; older historical rows do not keep an org in a warning state once the latest version is signed. |
| 60 | +- User can open a CLA from the list and view full language on `/sign/[orgSlug]`. |
| 61 | +- Data detail: full SHA-256 hash is persisted in DB; UI currently shows the short 7-character version label. |
| 62 | + |
| 63 | +### 4) Admin creates a new CLA version |
| 64 | + |
| 65 | +- Saving CLA updates current text/hash. |
| 66 | +- While editing, maintainers can preview rendering before save (`Edit`, `Split`, `Preview` modes; desktop defaults to split view). |
| 67 | +- Existing signatures remain historical; users on previous hash are treated as outdated and must re-sign. |
| 68 | +- If no one has signed prior versions, only current hash/text changes. |
| 69 | +- If prior versions were signed, historical signed versions remain in archives/signatures. |
| 70 | +- Saving CLA schedules an async workflow recheck of open pull requests; non-member contributors with missing/outdated signatures receive failing checks and updated signing comments once the workflow run completes. |
| 71 | + |
| 72 | +### 5) Contributor opens a PR |
| 73 | + |
| 74 | +- Org member: check passes, no CLA comment. |
| 75 | +- Personal-account repository owner: check passes, no CLA comment. |
| 76 | +- User or app/bot on org bypass list: check passes, no CLA comment. |
| 77 | +- Non-member + current signature: check passes, no CLA comment. |
| 78 | +- Non-member + outdated signature: check fails, re-sign comment posted. |
| 79 | +- Non-member + never signed: check fails, sign prompt comment posted. |
| 80 | + |
| 81 | +### 6) Contributor signs/re-signs CLA |
| 82 | + |
| 83 | +- Signature is stored with org, user, full CLA hash, accepted hash, assent metadata, immutable GitHub ID at signing time, timestamp, email provenance, and session evidence fields. |
| 84 | +- If `repo` + `pr` is provided, the signer must match that PR author before targeted PR sync is applied. |
| 85 | +- After signing/re-signing, the app schedules an async workflow to sync open PRs authored by that contributor in the org: latest CLA check runs are updated to success and stale CLA prompt comments are deleted. |
| 86 | + |
| 87 | +### 7) Signed CLA versions cannot be deleted |
| 88 | + |
| 89 | +- There is no route to delete signed CLA archives/signature history. |
| 90 | +- Signed version history behaves as append-only. |
| 91 | + |
| 92 | +### 8) CLA downloads |
| 93 | + |
| 94 | +- Contributors can download CLA versions from their own signing history. |
| 95 | +- Admins can download both current and archived CLA versions for orgs they administer. |
| 96 | +- Download endpoints enforce ownership/authorization and do not expose records across users/orgs. |
| 97 | + |
| 98 | +### 9) Additional scenarios commonly missed |
| 99 | + |
| 100 | +- Org deactivated/uninstalled: signing blocked; webhook events set passing CLA checks and remove managed CLA prompts so PRs are not blocked by CLA while inactive. |
| 101 | +- Activating or deactivating an org schedules an async open-PR recheck workflow so existing PR checks/comments converge automatically. |
| 102 | +- Updating either bypass section (users or app/bots) schedules async open-PR recheck so existing PRs converge to the latest policy. |
| 103 | +- `/recheck` authorization: allowed for PR author, org member, or maintainer; unauthorized users are blocked. |
| 104 | +- OAuth and install redirects sanitize `returnTo` to prevent open redirects. |
| 105 | +- Webhook hardening: production signature verification and delivery de-duplication. |
| 106 | +- Standard error paths: unauthorized, forbidden, missing org, invalid payload combinations. |
| 107 | + |
| 108 | +### 10) OAuth/session lifecycle edge cases |
| 109 | + |
| 110 | +- OAuth callback state mismatch/expired state cookie: sign-in fails safely and redirects back to `/auth/signin?error=...`. |
| 111 | +- GitHub token exchange or profile fetch failure: sign-in fails safely and no session cookie is issued. |
| 112 | +- Explicit logout clears JWT cookie; expired/invalid JWT is treated as signed-out. |
| 113 | + |
| 114 | +### 11) `/recheck` command behavior edge cases |
| 115 | + |
| 116 | +- `/recheck` is only processed on PR issue comments; non-PR issue comments are ignored. |
| 117 | +- Non-command comments (or non-created comment events) are ignored. |
| 118 | +- If PR head SHA cannot be resolved in production, `/recheck` fails with an error instead of guessing. |
| 119 | + |
| 120 | +### 12) Webhook delivery/idempotency scenarios |
| 121 | + |
| 122 | +- Duplicate `x-github-delivery` IDs are ignored via persistent DB-backed delivery tracking to reduce duplicate check/comment churn across process restarts. |
| 123 | +- Missing/invalid webhook signature is rejected in production when `GITHUB_WEBHOOK_SECRET` is configured. |
| 124 | +- Missing required payload fields return `400` and do not mutate DB/check state. |
| 125 | + |
| 126 | +### 13) Installation lifecycle scenarios |
| 127 | + |
| 128 | +- Installation `created` or `unsuspend`: account row is created/reactivated, installation ID refreshed, and installation target metadata (`organization` vs `user`) is persisted. |
| 129 | +- New installations are initialized with empty CLA text and `cla_text_sha256 = null` (no built-in agreement/template is auto-published). |
| 130 | +- Installation `deleted` or `suspend`: account is deactivated and installation ID cleared. |
| 131 | +- Installation repository-change events refresh installation linkage. |
| 132 | + |
| 133 | +### 14) Access-control scenarios |
| 134 | + |
| 135 | +- In production, org installs require live GitHub org-admin verification. |
| 136 | +- In production, personal-account installs are authorized when the signed-in GitHub user matches the installation target account. |
| 137 | +- In local dev/test, org-admin verification is relaxed to keep tests deterministic. |
| 138 | +- `/admin/[orgSlug]` and `/sign/[orgSlug]` handle unknown orgs with explicit not-found states. |
| 139 | + |
| 140 | +## System Behavior Spec (PR/Webhook Flow) |
| 141 | + |
| 142 | +- Pull request webhook checks whether PR author is an org member or has signed current CLA. |
| 143 | +- PR signature resolution is keyed by immutable GitHub user ID when available (username is fallback only). |
| 144 | +- Contributor dashboard status uses the latest stored signature per org to determine current/outdated state in UI. |
| 145 | +- Outcomes: |
| 146 | + - Org member: passing check, no CLA comment. |
| 147 | + - Bypass-listed user/app/bot: passing check, no CLA comment. |
| 148 | + - Signed current CLA: passing check, no CLA comment. |
| 149 | + - Unsigned/outdated signature: failing check + bot comment with signing URL. |
| 150 | +- App/bot bypass matching is slug-based and treats `<slug>` and `<slug>[bot]` as equivalent actor forms. |
| 151 | +- When CLA text changes, contributors on older signatures are marked as requiring re-sign; open PRs may require check re-evaluation and failure until re-signing. |
| 152 | +- After signing/re-signing, an async workflow updates signer-authored open PR CLA checks to success and removes stale CLA prompt comments. |
| 153 | +- Activating/deactivating CLA enforcement schedules async open-PR rechecks; inactive mode converges CLA checks to success and clears managed CLA prompt comments. |
| 154 | +- CLA bot comment updates/deletions are restricted to CLA-managed comments tagged with an internal signature marker, preventing edits to third-party bot comments. |
| 155 | +- Repository maintainers must require `CLA Bot / Contributor License Agreement` in GitHub branch protection/rulesets for merge blocking to be enforced. |
| 156 | +- Markdown ordered lists preserve explicit authored numbering (for example `1.`, `2.`, `7.` stays `1, 2, 7`), and legal alpha markers (`a.` / `a)`) render as ordered sub-clauses with indentation. |
| 157 | + |
| 158 | +## Test Coverage |
| 159 | + |
| 160 | +Reference UI coverage: |
| 161 | + |
| 162 | +- `tests/e2e/pages-reference.spec.ts` |
| 163 | + - `home and dashboard pages render core navigation` |
| 164 | + - `sign-in page sanitizes external returnTo` |
| 165 | + - `admin page shows auth-gated state when signed out` |
| 166 | + - `admin list and org detail pages render for signed-in admin` |
| 167 | + - `contributor page shows auth-gated state when signed out` |
| 168 | + - `contributor page shows signed agreements when signed in` |
| 169 | + - `sign page handles auth-gated and not-found states` |
| 170 | + - `sign page supports re-sign flow after CLA update` |
| 171 | + |
| 172 | +Reference API/flow coverage: |
| 173 | + |
| 174 | +- `tests/integration/api-suite.test.ts` |
| 175 | + - Broad integration flow coverage for auth/session APIs, org management, signing/re-signing, webhook checks/comments, install/uninstall/suspend lifecycle, and `/recheck` authorization. |
| 176 | + - Includes edge cases like stale-signature detection after CLA updates, `/recheck` handling/authorization on open PRs, malformed webhook payload rejection, and duplicate webhook delivery de-duplication. |
| 177 | + |
| 178 | +## Keeping Spec and Tests in Sync |
| 179 | + |
| 180 | +When behavior changes: |
| 181 | + |
| 182 | +1. Update the relevant sections in this file. |
| 183 | +2. Update or add coverage in `tests/integration/api-suite.test.ts` and/or `tests/e2e/pages-reference.spec.ts` as appropriate. |
| 184 | +3. Run `pnpm test` and `pnpm build` before merging. Run `pnpm test:all` when UI/browser behavior changes. |
0 commit comments