Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 167 additions & 10 deletions .github/workflows/cla.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CLA
name: ✍️ CLA

on:
pull_request:
Expand All @@ -8,29 +8,36 @@ on:
- opened
- reopened
- synchronize
issue_comment:
types:
- created
- edited

permissions:
contents: read
pull-requests: write

jobs:
cla:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
name: CLA
name: ✍️ CLA
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout base
- name: 📦 Checkout base
uses: actions/checkout@v6.0.2
with:
ref: ${{ github.event.pull_request.base.ref }}
persist-credentials: false

- name: Setup Node
- name: 🟢 Setup Node
uses: actions/setup-node@v6
with:
node-version: 22

- name: Collect identities
- name: 🧾 Collect identities
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
Expand Down Expand Up @@ -63,7 +70,7 @@ jobs:
`${JSON.stringify(identities, null, '\t')}\n`,
)

- name: Check CLA
- name: ✍️ Check CLA
id: check
run: |
if [ ! -f tools/ci/check-cla.mjs ] || [ ! -f .github/cla-signers.json ]; then
Expand All @@ -80,7 +87,7 @@ jobs:
exit "$code"
fi

- name: Comment when unsigned
- name: 💬 Comment when unsigned
if: steps.check.outputs.exit_code == '1'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
Expand All @@ -91,7 +98,7 @@ jobs:

1. Read the [Individual CLA](https://github.com/${{ github.repository }}/blob/main/docs/legal/individual-cla.md) (or the [Entity CLA](https://github.com/${{ github.repository }}/blob/main/docs/legal/entity-cla.md) if an organization owns the work).
2. Comment exactly: \`I have read the CLA and I hereby sign the CLA\`
3. A maintainer records your GitHub username on \`main\`. See [Inbound contributions](https://github.com/${{ github.repository }}/blob/main/docs/contributing/inbound-contributions.md).
3. The CLA workflow records your GitHub username on \`main\` and re-runs this check. See [Inbound contributions](https://github.com/${{ github.repository }}/blob/main/docs/contributing/inbound-contributions.md).

Adding your own username on this branch does not pass the check.`
const comments = await github.paginate(
Expand Down Expand Up @@ -122,6 +129,156 @@ jobs:
})
}

- name: Fail when unsigned
- name: ❌ Fail when unsigned
if: steps.check.outputs.exit_code == '1'
run: exit 1

record:
if: >
github.event_name == 'issue_comment' && github.event.issue.pull_request &&
github.event.comment.user.type == 'User' &&
contains(github.event.comment.body, 'I hereby sign the CLA')
runs-on: ubuntu-latest
name: 📝 Record CLA
timeout-minutes: 5
concurrency:
group: cla-signers-main
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
actions: write
steps:
- name: 📦 Checkout main
uses: actions/checkout@v6.0.2
with:
ref: main

- name: 🟢 Setup Node
uses: actions/setup-node@v6
with:
node-version: 22

- name: 📝 Record signer
id: record
env:
CLA_COMMENT: ${{ github.event.comment.body }}
CLA_LOGIN: ${{ github.event.comment.user.login }}
CLA_SIGNED_AT: ${{ github.event.comment.created_at }}
run: |
if [ ! -f tools/ci/check-cla.mjs ] || [ ! -f .github/cla-signers.json ]; then
echo "skipped=true" >> "$GITHUB_OUTPUT"
echo "added=false" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s' "$CLA_COMMENT" > cla-comment.txt
node tools/ci/check-cla.mjs \
--signers .github/cla-signers.json \
--record-signer "$CLA_LOGIN" \
--signed-at "${CLA_SIGNED_AT%%T*}" \
--comment-file cla-comment.txt > cla-record-output.txt
cat cla-record-output.txt
grep -E '^(skipped|added|github)=' cla-record-output.txt >> "$GITHUB_OUTPUT"

- name: 💾 Commit signer on main
if: steps.record.outputs.added == 'true'
env:
CLA_GITHUB: ${{ steps.record.outputs.github }}
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add .github/cla-signers.json
git commit -m "Record @${CLA_GITHUB} as an individual CLA signer"
git pull --rebase origin main
git push origin HEAD:main

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shallow clone breaks signer rebase push

Medium Severity

The record job checks out main with the default shallow fetch-depth of 1, then runs git pull --rebase before pushing the new signer commit. If main moved during the job (for example a merged PR), the shallow rebase often fails, so the signature never lands on main and the CLA check stays red.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1096e58. Configure here.


- name: 💬 Comment and re-run CLA
if: steps.record.outputs.skipped == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
CLA_ADDED: ${{ steps.record.outputs.added }}
CLA_GITHUB: ${{ steps.record.outputs.github }}
with:
script: |
const login = process.env.CLA_GITHUB
const added = process.env.CLA_ADDED === 'true'
const marker = '<!-- kody-cla-recorded -->'
const owner = context.repo.owner
const repo = context.repo.repo
const pull = await github.rest.pulls.get({
owner,
repo,
pull_number: context.payload.issue.number,
})
const headSha = pull.data.head.sha

async function latestClaRun() {
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'cla.yml',
event: 'pull_request',
head_sha: headSha,
per_page: 5,
})
return runs.data.workflow_runs[0] ?? null
}

const deadline = Date.now() + 180_000
let latest = await latestClaRun()
while (latest && latest.status !== 'completed' && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 10_000))
latest = await latestClaRun()
}

let reran = false
if (latest?.status === 'completed') {
try {
await github.rest.actions.reRunWorkflow({
owner,
repo,
run_id: latest.id,
})
reran = true
} catch (error) {
core.warning(
`Could not re-run CLA: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

const followUp = reran
? 'The CLA check will re-run.'
: 'The next CLA check will read the updated signers file.'
const body = added
? `${marker}
Recorded @${login} as an individual CLA signer on \`main\`. ${followUp}`
: `${marker}
@${login} is already recorded as an individual CLA signer. ${followUp}`
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: context.payload.issue.number,
per_page: 100,
},
)
const existing = comments.find((comment) =>
comment.body?.includes(marker),
)
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
})
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: context.payload.issue.number,
body,
})
}
9 changes: 5 additions & 4 deletions docs/contributing/decisions/0001-inbound-cla.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ How it works:
- **Which form.** [Individual CLA](../../legal/individual-cla.md) by default.
[Entity CLA](../../legal/entity-cla.md) when an organization owns the work.
- **How they sign.** Read the CLA, then comment:
`I have read the CLA and I hereby sign the CLA`. A maintainer records the
GitHub username in `.github/cla-signers.json` on `main`.
`I have read the CLA and I hereby sign the CLA`. The `CLA` workflow,
running from `main` on that comment, records the commenter's GitHub
username in `.github/cla-signers.json` on `main` and re-runs the check.
- **Enforcement.** The `CLA` workflow reads signers from `main` (never
from the pull request head) and fails closed. No trivial-contribution
exception.
Expand All @@ -52,7 +53,7 @@ How it works:

- The Licensor stays one party for FSL, the Apache conversion, relicensing,
and enforcement.
- Outside pull requests take one extra maintainer step (record the signer on
`main`).
- Individual signatures do not need a maintainer to edit the signers file.
The recorder is a first-party `issue_comment` job that writes `main` only.
- Revisit if the Licensor becomes a company, if counsel revises the CLA
text, or if contribution volume justifies hosted click-to-sign.
20 changes: 10 additions & 10 deletions docs/contributing/inbound-contributions.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ There is no exception for docs-only or one-line patches.
I have read the CLA and I hereby sign the CLA
```

4. Wait for a maintainer to record your GitHub username in
[`.github/cla-signers.json`](../../.github/cla-signers.json) on `main`.
5. Re-run the `CLA` check, or push another commit.
4. The `CLA` workflow records your GitHub username in
[`.github/cla-signers.json`](../../.github/cla-signers.json) on `main` and
re-runs the check.

Signing once covers past, present, and future contributions from that GitHub
identity. People who contributed before this process sign the same way before
their next merge. The `CLA` workflow reads signers from `main`, not from
the pull request branch, so adding your own username on the branch does not
pass the check.
their next merge. The workflow reads signers from `main`, not from the pull
request branch, so adding your own username on the branch does not pass the
check. Only the commenter is recorded, and only from the exact phrase.

## How to sign (entity)

Expand Down Expand Up @@ -70,10 +70,10 @@ authors the pull request as `kentcdodds` and the commits as `cursoragent`.

Do not merge a pull request while the `CLA` check is red.

After a valid individual signing comment, add a `signers` entry on `main`
(GitHub login, `signedAt` as an ISO date, `cla` of `individual`) and
re-run the check. After an accepted Entity CLA, add each authorized username
with `cla` of `entity`.
Individual signatures are recorded automatically from the signing comment.
After an accepted Entity CLA, add each authorized username to
`.github/cla-signers.json` on `main` with `signedAt` as an ISO date
(`YYYY-MM-DD`) and `cla` of `entity`.

Make the `CLA` check required for `main` in GitHub branch protection so a
green review cannot skip it.
Expand Down
Loading