Skip to content

Repository files navigation

cla-tool

A small Contributor License Agreement (CLA) bot for GitHub, packaged as a reusable Action. It asks each contributor to sign a short agreement on their pull request before it can be merged, and remembers who has signed.

It was built for yjs/yhub and is intentionally simple: a few hundred lines of plain ESM JavaScript on top of fetch and lib0 — no Octokit, no server to run.

What's a CLA, and why does this exist?

A Contributor License Agreement is a short agreement where a contributor confirms two simple things: "I wrote this" and "I give the project permission to use and license it." That's it.

Here's the why, plainly: we want yhub to be genuinely open source under the AGPL. We also want to be able to sell a proprietary license (and proprietary plugins) to companies who can't or don't want to use the AGPL. That dual model — free and open for everyone, paid for those who need different terms — is, after a lot of thought, the best way we've found to make a living from open source and keep investing in it full-time.

To offer that proprietary option, we need each contributor's permission to include their code in both editions. Without it, a single outside contribution we couldn't relicense would block the proprietary edition. The CLA is simply how we get that permission, up front and on the record.

What you keep: you keep the copyright to your contribution — this is a license, not a transfer of ownership. And everything you contribute always stays available under the AGPL. The CLA just additionally lets us offer it under a commercial license too. We use the well-known Harmony agreement (Option Five) for exactly this; the full text is in CLA.md.

What it does

  • Runs as a GitHub Action on pull_request_target and issue_comment.
  • Works out which contributors on a PR need to sign (by their GitHub account).
  • Posts a single, tidy PR comment with a checklist and signing instructions.
  • Lets contributors sign by commenting a phrase on the PR.
  • Records signatures as JSON on a dedicated branch in your repo.
  • Sets a license/cla commit status so a branch-protection rule blocks merging until everyone has signed.

How signing works

The bot comments on the PR asking unsigned contributors to sign. A contributor signs by posting a comment containing exactly:

I have read the CLA Document and I hereby sign the CLA

The bot records it and flips the license/cla status to ✅. Comment recheck to re-evaluate (e.g. after fixing a commit author). Signatures are versioned by a hash of CLA.md: if you ever change the agreement text, everyone re-signs the new version.

Inspiration

This is inspired by CLA Assistant Lite, which is no longer maintained (archived in 2026). The signing flow and the same signatures file shape ({ "signedContributors": [...] }) are deliberately familiar. The main difference is how merges are gated: CLA Assistant Lite relied on the workflow run's own pass/fail and re-ran it with a Personal Access Token; cla-tool instead sets an explicit commit status on the PR head, so no PAT and no workflow re-run are needed.

Quick start (adding it to a repo)

  1. Add the workflow .github/workflows/cla.yml (see examples/yhub):

    name: CLA
    on:
      pull_request_target: { types: [opened, synchronize, reopened] }
      issue_comment:       { types: [created] }
    permissions:
      contents: write        # commit signatures to the signatures branch
      pull-requests: write   # post/update the sticky comment
      statuses: write        # set the license/cla commit status
    jobs:
      cla:
        if: >-
          github.event_name == 'pull_request_target' ||
          (github.event.issue.pull_request != null &&
           (contains(github.event.comment.body, 'I hereby sign the CLA') ||
            github.event.comment.body == 'recheck'))
        runs-on: ubuntu-latest
        steps:
          - uses: yjs/cla-tool@v1
            with:
              cla-document-path: CLA.md
              allowlist: dependabot[bot],github-actions[bot],dmonad
  2. Add the agreement CLA.md (and optionally CLA-ENTITY.md) and a CONTRIBUTING.md to your repo's default branch.

  3. Make the check required. In Settings → Branches → branch protection for main, enable Require status checks to pass and add license/cla.

  4. That's it. The cla-signatures branch is created automatically on the first signature (keep it unprotected). The default repo token is enough — you only need a PAT if you store signatures in a different repository.

The if: guard keeps the job from running on every comment and avoids interfering with other comment-triggered workflows. GitHub does not re-trigger workflows from comments the bot itself posts, so there is no signing loop.

Inputs

Input Default Description
github-token ${{ github.token }} Token to read the PR, comment, and set the status.
personal-access-token '' Only needed to store signatures in a different repo.
cla-document-path CLA.md Path (on the base branch) to the agreement text.
sign-phrase I have read the CLA Document and I hereby sign the CLA Phrase a contributor comments to sign (case-insensitive).
recheck-phrase recheck Comment this to re-evaluate without signing.
allowlist '' Comma/space-separated logins exempt from signing (bots are always exempt).
base-branch main Only enforce the CLA on PRs targeting this branch.
signatures-branch cla-signatures Unprotected branch holding the signatures file (auto-created).
signatures-path version1/cla.json Path of the signatures JSON on that branch.
status-context license/cla Commit-status name to set (make it a required check).

Signatures file

Stored on the cla-signatures branch at version1/cla.json:

{
  "signedContributors": [
    { "login": "alice", "id": 7, "pullRequestNo": 42, "commentId": 100, "createdAt": "2026-06-24T00:00:00Z", "claVersion": "e112683956a7…" }
  ]
}

claVersion is the SHA-256 of CLA.md, so a signature only counts for the exact text that was agreed to.

Security

The workflow uses pull_request_target, which runs with a write-capable token even for PRs from forks (that's why the bot can comment and set a status on outside contributions). cla-tool only ever reads PR metadata and writes a comment/status — it never checks out or runs the PR's code, which is the one thing you must avoid under pull_request_target.

Development

npm install
npm test          # lib0/testing unit + end-to-end (mocked GitHub) tests
npm run lint      # standard
npm run build     # bundle src + lib0 -> dist/index.mjs (commit this; the Action runs it)

dist/index.mjs is committed on purpose: GitHub does not npm install for JavaScript actions, so the bundle is what runs. Rebuild it whenever you change src/.

Licensing

  • This tool (the code) is MIT licensed — see LICENSE.
  • The agreement texts CLA.md / CLA-ENTITY.md are the Harmony agreements (v1.0), reproduced under CC BY 3.0 with the standard Option Five selection and project placeholders filled in.
  • Nothing here is legal advice. If you adapt the CLA for your own project, consider a quick review by a lawyer — especially the governing-law jurisdiction.

Credits

Inspired by CLA Assistant Lite and the original CLA Assistant. CLA text by the Harmony Agreements project.

About

GitHub Actions CLA Assistant

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages