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
2 changes: 2 additions & 0 deletions .github/workflows/cla-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ jobs:
'- 📋 **Individual?** → [Sign CLA Individual](https://codecoradev.github.io/cla/?type=individual)',
'- 🏢 **Corporate?** → [Sign CLA Corporate](https://codecoradev.github.io/cla/?type=corporate)',
'',
'After submitting, wait for the green **✅ CLA submitted** confirmation — that means your signature went through. It is recorded automatically and this check will pass within a few minutes.',
'',
'---',
'<sub>By signing, you agree to the terms in [CLA_INDIVIDUAL.md](https://github.com/codecoradev/.github/blob/main/CLA_INDIVIDUAL.md) or [CLA_CORPORATE.md](https://github.com/codecoradev/.github/blob/main/CLA_CORPORATE.md).</sub>',
].join('\n');
Expand Down
165 changes: 165 additions & 0 deletions .github/workflows/cla-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: CLA Sync

# Consumes [CLA] Signature issues created by the signing portal
# (https://codecoradev.github.io/cla/), validates them against the claimed
# GitHub identity, appends the signature to .cla/signatures.json, and opens
# an auto-generated PR for owner review.
#
# Why server-side: contributors authenticate to the portal with a
# non-push OAuth token, so the portal itself must not (and cannot) apply
# repo labels. Labeling and the signatures.json write both happen here,
# with a PAT that has write access to this repository.

on:
issues:
types: [opened]

permissions:
issues: write
contents: write
pull-requests: write

concurrency:
group: cla-sync-${{ github.event.issue.number }}
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-latest
if: contains(github.event.issue.labels.*.name, 'cla-signature') || startsWith(github.event.issue.title, '[CLA] Signature:')
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate & apply signature
id: apply
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const author = issue.user.login;
const body = issue.body || '';

// 1. Parse fields from the portal-generated issue body.
const field = (label) => {
const m = body.match(new RegExp(`- \\*\\*${label}:\\*\\* (.+)`));
return m ? m[1].trim() : null;
};
const name = field('Name');
const claimedUser = (field('GitHub') || '').replace(/^@/, '');
const claType = field('CLA Type');
const signature = field('Signature');

if (!name || !claimedUser || !claType || !signature) {
core.setOutput('result', 'invalid');
core.setOutput('reason', 'Missing required fields (Name / GitHub / CLA Type / Signature).');
return;
}

// 2. Identity binding: the issue opener must be the claimed user.
// (Portal fills the username via OAuth — this guards against
// hand-crafted issues signing for someone else.)
if (author.toLowerCase() !== claimedUser.toLowerCase()) {
core.setOutput('result', 'rejected');
core.setOutput('reason', `Issue opened by @${author} but claims signature for @${claimedUser}.`);
return;
}

// 3. Known CLA types only.
if (!['individual', 'corporate'].includes(claType)) {
core.setOutput('result', 'invalid');
core.setOutput('reason', `Unknown CLA type: ${claType}`);
return;
}

// 4. Merge into signatures.json (idempotent per username).
const fs = require('fs');
const path = '.cla/signatures.json';
const db = JSON.parse(fs.readFileSync(path, 'utf8'));
db.signatures = (db.signatures || []).filter(
(s) => s.github_username.toLowerCase() !== author.toLowerCase()
);
db.signatures.push({
name,
github_username: author,
signed_at: new Date().toISOString(),
cla_type: claType,
commit_sha: context.sha.slice(0, 7),
signature,
});

fs.writeFileSync(path, JSON.stringify(db, null, 2) + '\n');
core.setOutput('result', 'ok');
core.setOutput('username', author);

- name: Label issue (server-side)
if: steps.apply.outputs.result == 'ok'
run: gh issue edit ${{ github.event.issue.number }} --add-label cla-signature --repo codecoradev/.github
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create signature PR
if: steps.apply.outputs.result == 'ok'
id: pr
run: |
BRANCH="cla/signature-${{ steps.apply.outputs.username }}"
git config user.name "cla-sync[bot]"
git config user.email "cla-sync[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add .cla/signatures.json
git commit -m "cla: add signature for @${{ steps.apply.outputs.username }} (issue #${{ github.event.issue.number }})"
# Org caps GITHUB_TOKEN at read — push with the PAT explicitly.
git push "https://x-access-token:${GH_TOKEN}@github.com/codecoradev/.github.git" "$BRANCH"
PR_URL=$(gh pr create \
--repo codecoradev/.github \
--head "$BRANCH" \
--base main \
--title "cla: add signature for @${{ steps.apply.outputs.username }}" \
--body "## What
- Adds CLA signature for @${{ steps.apply.outputs.username }} to \`.cla/signatures.json\`
- Source: signing portal issue #${{ github.event.issue.number }} (identity verified: issue opener == claimed GitHub user)

## Why
- Automates the manual signature-recording step; the \`cla-check\` workflow in product repos reads this file.

## Testing
- \`cla-sync\` validation passed (fields parsed, identity match, CLA type known)
- Owner merges this PR to complete the CLA flow")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ secrets.OWNER_PAT }}

- name: Comment result on issue
if: always()
uses: actions/github-script@v7
env:
SYNC_RESULT: ${{ steps.apply.outputs.result }}
SYNC_REASON: ${{ steps.apply.outputs.reason }}
SYNC_PR_URL: ${{ steps.pr.outputs.pr_url }}
with:
script: |
// Read via env, never via ${{ }} interpolation — issue-derived
// values are attacker-controlled and must not enter the script
// source (GitHub Actions script injection).
const result = process.env.SYNC_RESULT;
const prUrl = process.env.SYNC_PR_URL;
const reason = process.env.SYNC_REASON || 'unknown error';
if (result === 'ok' && prUrl) {
msg = `✅ Signature validated and recorded.\n\n📎 Review & merge: ${prUrl}\nOnce merged, the \`cla-check\` workflow will pass for your PRs. No further action needed.`;
} else if (result === 'ok') {
msg = `✅ Signature validated, but the automated PR could not be created — a signature PR for your account is probably already pending. A maintainer will finalize it shortly.`;
} else {
msg = `⚠️ Could not process this signature automatically: ${reason}\n\nPlease re-submit via https://codecoradev.github.io/cla/ (make sure you sign in with GitHub first), or contact a maintainer.`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: msg,
});

- name: Close processed issue
if: steps.apply.outputs.result == 'ok' && steps.pr.outputs.pr_url != ''
run: gh issue close ${{ github.event.issue.number }} --repo codecoradev/.github --reason completed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading