Centralized GitHub Actions automation for the Cattr Contributor License Agreement (CLA) flow.
This public repository contains reusable workflows and a TypeScript GitHub Action used by Cattr repositories that require CLA acceptance. Source repositories keep only their own CLA.md and a small caller workflow. CLA snapshots, authorship claims, and acceptance records are stored separately in the private cattr-app/cla-registry repository.
The action has no runtime npm dependencies. GitHub API access uses the Node.js 20 built-in fetch API. TypeScript is bundled with esbuild into a single committed dist/index.js, so caller repositories do not install dependencies or build the action.
CLA-enabled repository
├── CLA.md
└── .github/workflows/cla.yml
│
▼
cattr-app/cla-actions
├── reusable workflows
└── TypeScript action
│
├── Cattr CLA Bot
│ ├── reads source repositories
│ ├── reads cla-registry
│ ├── posts PR comments
│ └── creates Cattr CLA checks
│
└── Cattr CLA Registry
└── writes cla-registry
│
▼
private cla-registry
Two GitHub Apps intentionally separate permissions:
- Cattr CLA Bot is contributor-facing and cannot modify repository contents.
- Cattr CLA Registry can write registry contents and is installed only on
cattr-app/cla-registry.
.
├── .github/
│ └── workflows/
│ ├── check.yml
│ ├── sign.yml
│ ├── claim.yml
│ ├── sync.yml
│ └── ci.yml
├── src/
│ ├── operations/
│ │ ├── check.ts
│ │ ├── sign.ts
│ │ ├── claim.ts
│ │ └── sync.ts
│ ├── cla.ts
│ ├── config.ts
│ ├── contributors.ts
│ ├── github.ts
│ ├── registry.ts
│ ├── runtime.ts
│ ├── types.ts
│ └── index.ts
├── dist/
│ └── index.js
├── tests/
│ └── cla.test.ts
├── examples/
│ └── cla.yml
├── AGENTS.md
├── action.yml
├── package.json
└── tsconfig.json
A pull request passes only when every human contributor represented by its commits has accepted the effective CLA version for that repository.
The check is not limited to the pull request author.
GitHub GraphQL Commit.authors is used so both the primary Git author and identities attributed through Co-authored-by trailers are included.
Resolved GitHub users are de-duplicated by numeric GitHub user ID. Usernames are descriptive metadata because they can change.
The default explicit automation exemption is:
dependabot[bot]
Additional exemptions can be supplied through a newline-separated CLA_EXEMPT_LOGINS environment variable. Bot-like usernames are never automatically trusted.
Sometimes GitHub cannot associate a Git author or co-author identity with a GitHub account. Such identities are not silently ignored.
The bot reports an unresolved identity with a command such as:
/cla-claim abc123def456 deadbeef1234
A claim means that the commenting GitHub account explicitly claims authorship/co-authorship of that exact unresolved identity in that exact commit.
A claim does not itself accept the CLA. The claimant still accepts the effective agreement with:
/cla-sign 1
The registry does not persist the unresolved Git email. The claim key is a SHA-256 fingerprint of the exact Git name/email pair, calculated in memory.
If multiple GitHub users claim the same unresolved identity, the check fails for maintainer review. The automation never chooses between competing claims.
Prefer fixing commit authorship and pushing rewritten commits when practical. Do not claim third-party commits.
Each participating repository keeps:
/CLA.md
with exactly one marker:
<!-- cattr-cla-version: 1 -->
# Cattr Contributor License AgreementVersions are monotonically increasing integers. Any textual modification requires a new version.
For a pull request, the effective CLA is loaded from trusted repository history, never from contributor-controlled head content.
Normally the CLA is pinned to the PR base commit. For legacy pull requests whose base commit predates the introduction of CLA.md, the action resolves the current target branch to a concrete commit SHA and loads the CLA from that pinned commit instead. The fallback is used only when the CLA file is absent from the original base revision; malformed or mismatched CLA data does not fall through to a newer version.
A PR that changes CLA.md remains governed by the version in its base revision. The new version becomes effective after merge to the default branch and synchronization to the registry.
cla-registry/
├── agreements/
│ └── <repository-id>/
│ └── <cla-version>/
│ ├── CLA.md
│ └── metadata.json
├── acceptances/
│ └── <github-user-id>/
│ └── <repository-id>/
│ └── <cla-version>.json
└── claims/
└── <repository-id>/
└── <commit-sha>/
└── <identity-sha256>/
└── <github-user-id>.json
Agreement snapshots and acceptance records are append-only by automation. Existing records are validated rather than overwritten.
Install on every CLA-enabled repository and on cattr-app/cla-registry.
Repository permissions:
Contents: Read-only
Issues: Read & write
Pull requests: Read-only
Checks: Read & write
Metadata: Read-only
Install only on cattr-app/cla-registry.
Repository permissions:
Contents: Read & write
Metadata: Read-only
Webhooks, OAuth user authorization, and client secrets are not required. GitHub Actions creates short-lived installation tokens from the private keys.
Variables:
CATTR_CLA_BOT_CLIENT_ID
CATTR_CLA_REGISTRY_CLIENT_ID
Secrets:
CATTR_CLA_BOT_PRIVATE_KEY
CATTR_CLA_REGISTRY_PRIVATE_KEY
Create CLA.md, then copy examples/cla.yml to:
.github/workflows/cla.yml
Install Cattr CLA Bot on the repository.
After the first PR creates the custom Cattr CLA check, make that check required for the protected default branch.
Normal resolved contributor:
PR opened
↓
CLA check
↓
/cla-sign 1
↓
acceptance recorded
↓
check reevaluated
Unresolved Git identity:
PR opened
↓
unresolved identity reported
↓
/cla-claim <commit> <identity>
↓
GitHub account associated with that commit identity
↓
/cla-sign 1
↓
acceptance recorded
↓
check reevaluated
For a multi-author PR, every human author and co-author must independently satisfy the CLA check.
The caller uses pull_request_target so fork PRs can participate in the CLA flow while organization secrets remain available to the trusted workflow.
The action never checks out or executes the pull request head. The effective CLA is fetched through the GitHub API from either the trusted PR base SHA or, only when that revision has no CLA file, a concrete SHA resolved from the trusted target branch.
Do not change this invariant. In particular, CLA workflows must never execute contributor-controlled files, scripts, or actions.
/cla-claim is an explicit self-attestation rather than cryptographic proof of Git authorship. Conflicting claims block the check for maintainer review.
This repository uses pnpm, pinned through the packageManager field in package.json.
corepack enable
pnpm installpnpm is used only for development, tests, and building the action. Caller repositories execute the committed dist/index.js directly.
After the first pnpm install, commit the generated pnpm-lock.yaml. Once the lockfile exists, CI should use pnpm install --frozen-lockfile.
Type-check:
pnpm typecheckRun tests directly against the TypeScript sources:
pnpm testBuild the deployable GitHub Action:
pnpm buildThe build command is intentionally kept directly in package.json:
esbuild src/index.ts --bundle --platform=node --format=cjs --target=node20 --outfile=dist/index.js --legal-comments=none
There is no repository-level scripts/ directory.
dist/index.js is the only generated deployment artifact and must be committed because GitHub Actions executes it directly. Never edit dist/index.js manually; change src/ and rebuild it.
Before committing a change that affects the action:
pnpm check
pnpm build
git diff -- dist/index.jsCI type-checks the source, runs the tests, rebuilds the action with esbuild, and validates the generated JavaScript syntax.
Do not consume this repository from @main.
Publish immutable releases:
v1.0.0
v1.0.1
v1.1.0
and maintain a moving backwards-compatible major tag:
v1
Cattr repositories normally call:
uses: cattr-app/cla-actions/.github/workflows/check.yml@v1Breaking workflow, action-input, or registry-contract changes require a new major version.