GitHub's pull request model gives you multiple well-defined hooks for injecting compliance checks into the contribution workflow. CLA enforcement fits naturally into the PR lifecycle — but getting it right requires understanding which integration points actually block a merge versus which ones are advisory-only.
This guide walks through the complete setup: GitHub App installation, webhook event handling, commit status checks, and the edge cases that trip up most first implementations.
The GitHub Integration Model
GitHub offers two primary integration patterns for CLA enforcement:
- GitHub Apps: OAuth-scoped integrations that act as their own actors (with their own user identity in status checks and comments). GitHub Apps are the modern, preferred integration path. They use fine-grained permission scopes — you request exactly the permissions you need:
statuses:write,pull_requests:read,members:read. - OAuth Apps: Legacy integration model that acts on behalf of a user. Still works, but doesn't support fine-grained permissions. Avoid for new integrations.
For CLA enforcement, a GitHub App is the right choice. It can post commit statuses independently, comment on PRs as itself, and receive webhook events for the full PR lifecycle.
Webhook Events to Handle
CLA enforcement needs to respond to several PR lifecycle events. The minimum viable set:
pull_request.opened— Trigger initial CLA check for all authors in the PRpull_request.synchronize— Re-run check when new commits are pushed (new authors may appear)pull_request_review.submitted— Not needed for CLA; skip thisissue_comment.created— Catch "I agree" style re-trigger comments if you support that flow
The critical detail in pull_request.synchronize: when a contributor force-pushes or adds commits, the list of authors in the PR may change. A PR that was CLA-clear can become non-compliant if a new commit is authored by someone who hasn't signed. Your webhook handler must re-evaluate the full commit author list on every synchronize event, not just check the new commits.
Extracting Commit Authors from a PR
The GitHub REST API provides the commits list for a pull request at:
GET /repos/{owner}/{repo}/pulls/{pull_number}/commits
Each commit object includes an author field with the GitHub user object (if the email is linked to a GitHub account) and a committer field. For CLA purposes, the author is the relevant party — the person who wrote the code. The committer may be a bot or CI system.
An important edge case: commits authored with an email address that isn't associated with any GitHub account will return author: null. These are not anonymous — the commit still has an email — but you can't map them to a GitHub identity. Your CLA system needs a fallback: either flag these commits as unverifiable and block the PR, or maintain a separate email-to-CLA mapping table and check that.
Co-authored commits (using the Co-authored-by: trailer) add another complication: GitHub parses these trailers and includes co-authors in the commit's author list as of 2019, but the API representation varies by whether the co-author's email is linked to a GitHub account. Test your co-author handling explicitly.
Setting Commit Status Checks
The GitHub Commit Status API is what actually gates the merge. You post a status to the commit SHA at the head of the PR:
POST /repos/{owner}/{repo}/statuses/{sha}
{
"state": "pending" | "success" | "failure" | "error",
"target_url": "https://your-cla-app.com/sign?pr=...",
"description": "2 of 3 contributors have signed the CLA",
"context": "cohorto/cla"
}
The context field is your status check's identifier. Branch protection rules reference this context by name. Keep it stable — if you change the context string, existing branch protection rules silently stop matching it, and the check appears to disappear from the PR UI.
Set the initial status to pending immediately when your webhook handler receives the event. This prevents a race condition where the PR shows no status check at all between event receipt and your check completing. A PR with no status is typically in an ambiguous merge-allowed state depending on branch protection configuration.
Branch Protection Configuration
None of the above matters if branch protection isn't configured to require your status check. In the repository settings under Branch protection rules for the default branch (or whichever branches accept contributions), enable "Require status checks to pass before merging" and add your context string as a required check.
There's a gotcha with required status checks: GitHub only allows you to add a context to the required list after it has appeared on at least one PR in that repository. This creates a chicken-and-egg problem for initial setup. The workaround is to open a test PR, let the CLA app post its first status, then configure the branch protection rule. Only then will the check be enforced on subsequent PRs.
We're not saying branch protection is the only valid enforcement model — some teams prefer advisory checks (non-blocking status) plus CODEOWNERS review requirements as a softer gate. But if you need hard enforcement — PRs cannot merge without all CLAs signed — required status checks are the mechanism, not optional.
Handling the Signature Flow
When the CLA check fails, your status's target_url should take the contributor directly to the signing interface. The URL should carry enough context to pre-populate the form: at minimum, the contributor's GitHub username and the repo/PR they're contributing to.
After signature, you need to re-trigger the CLA check for the PR. Options:
- Re-evaluate on signature event: When a signature is recorded, your backend looks up all open PRs with pending CLA status that this contributor was blocking, and re-posts updated statuses.
- Periodic re-check: A background job that sweeps PRs with
failureCLA status and re-evaluates. Simpler, but introduces latency (contributor signs, then waits for the job cycle). - Re-trigger via comment: Instruct contributors to comment
/recheck-claor similar after signing. Low-tech but explicit. Requires your webhook handler to process issue comments.
Option 1 is the best experience but requires your system to maintain the PR→contributor mapping. Option 3 is the most common fallback seen in manual implementations.
Org-Level vs. Repo-Level Installation
GitHub Apps can be installed at the organization level (covering all repos in the org) or at the individual repository level. For most OSPOs, org-level installation with repo inclusion/exclusion controls is the right model. You don't want to configure branch protection and CLA checks separately for each of your 80 repositories.
One scenario worth planning for: org members who contribute to repos in your org through their personal accounts (not their org member account). The CLA check will see their personal GitHub username, not their work identity. Your contributor mapping needs to handle this — either by linking personal GitHub usernames to corporate identities at sign time, or by requiring contributors to contribute from their org-provisioned GitHub account.
Getting the GitHub integration right is the foundation. With a stable webhook handler, correct status context configuration, and well-tested co-author handling, the day-to-day operational burden of CLA enforcement moves from manual tracking to automated status — which is where it belongs.