A GitHub Action that requires every contributor to a pull request to sign a Contributor License Agreement before it can be merged. Signatures are stored as JSON in a git repository, so the ledger is auditable and lives where the rest of the history does.
It replaces contributor-assistant/github-action,
which was archived on 2026-03-23 and is now read-only. Its action.yml declares using: "node20",
and GitHub removes Node 20 from the runners on 2026-09-23.
Because the repository is archived, that line can never be changed.
Three defects in its final release (v2.6.1), all verified against
its src/graphql.ts,
are fixed here:
| Old behaviour | Here | |
|---|---|---|
| PR opener | Only commit author/committer were enumerated; pull_request.user was never read, so opening a PR carrying someone else's commits required no signature from the opener. |
The opener always signs. Submitting the contribution is the act the CLA governs. |
| Co-authors | Commit messages were never fetched, so Co-authored-by: trailers were invisible. |
Trailers are parsed; noreply addresses resolve to the account, anything else is reported. |
| Pagination | pageInfo.hasNextPage was selected but never used — only the first 100 commits were checked. |
Fully paginated. |
Unidentifiable authors — a commit whose email is linked to no GitHub account — now block the check instead of passing silently, and the contributor is told how to fix it.
name: CLA
on:
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
statuses: write
concurrency:
group: cla-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false
jobs:
cla:
runs-on: ubuntu-latest
# Bots cannot sign, so never nag them. `github.event.pull_request` is null on issue_comment and
# `github.event.comment` is null on pull_request_target; both then read as '' and pass.
if: >-
github.event.pull_request.user.type != 'Bot'
&& github.event.comment.user.type != 'Bot'
&& (github.event_name == 'pull_request_target'
|| (github.event.issue.pull_request
&& (github.event.comment.body == '/recheck'
|| contains(github.event.comment.body, 'I have read the CLA Document and I hereby sign the CLA'))))
steps:
- uses: argon-chat/cla@v1
with:
document-url: 'https://github.com/argon-chat/server/blob/master/.github/CLA.md'
signatures-path: '.github/signs/v2/cla.json'Point every repository at the same store and a contributor signs once for all of them:
with:
signatures-repository: 'argon-chat/cla'
signatures-path: 'signatures/v2/cla.json'The token then needs contents: write on argon-chat/cla, which GITHUB_TOKEN cannot have —
it is scoped to the repository running the workflow. Pass a PAT or App token as github-token.
| Input | Default | |
|---|---|---|
github-token |
${{ github.token }} |
Needs the three permissions: above. Only a restricted branch or a ledger in another repository requires a PAT. |
document-url |
— | Required. Link to the CLA text being agreed to. |
signatures-path |
.github/signs/v2/cla.json |
Path to the ledger. |
signatures-repository |
(same repo) | owner/repo holding the ledger. |
signatures-branch |
master |
Branch of the ledger. |
status-context |
license/cla |
Commit status name, for branch protection to require. |
sign-phrase |
I have read the CLA Document and I hereby sign the CLA |
Matched whole, trimmed, case-insensitively. |
allowlist |
(empty) | Comma-separated logins that never sign. Bots are exempt automatically. A trailing * matches a prefix. |
fail-on-unsigned |
true |
Fail the job, not just the status. |
| Output | |
|---|---|
signed |
"true" when everyone has signed and no author is unidentified. |
pending |
Comma-separated logins still owing a signature. |
Patterns are matched against the whole login, case-insensitively, with a trailing * meaning
"prefix". The action it replaces compiled each pattern into an unanchored regex, so the
bot* example in its own README also exempted a human whose login merely contained bot
— robotnik, say. You do not need to allowlist bots here at all: any account whose type is Bot
or whose login ends in [bot] is skipped, as is web-flow, the identity GitHub attributes to
commits made through the web UI.
GITHUB_TOKEN is enough for the common case, and it is the default. The calling workflow has to
declare the three permissions shown above; a repository whose default workflow permissions are
read-only does not need changing, because an explicit permissions: block raises them for that
workflow.
Reach for a PAT or App token only when:
- the ledger lives in another repository (
signatures-repository), whichGITHUB_TOKENcannot write to, or - the signature branch restricts who may push, via branch-protection Restrict who can push or a
ruleset, and
github-actions[bot]is not on the bypass list.
Requiring signed commits on the signature branch is not a reason to use a PAT: commits written through the contents API are signed by GitHub with its own key either way.
- Runs on
pull_request_target, which carries a writable token. The action never checks out or executes pull request code; it only reads the API. - Inputs reach the script as environment variables and are never interpolated into a
script:body, so an input cannot be parsed as code. - Only the account that posted the signing comment is ever recorded, and only if it actually owes a signature on that pull request. GitHub authenticates the comment author, so the phrase cannot be used to sign on someone else's behalf.
node --test "src/*.test.js" # unit tests, no dependencies
node scripts/verify-manifest.jsThere is no build step and no dist/ bundle: this is a composite action wrapping
actions/github-script, which supplies the authenticated Octokit client. Nothing to rebuild means
nothing to forget to rebuild.