From be6b8e24c8e25073cbb15ff6658e4a14c2affea2 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Thu, 10 Sep 2026 20:58:10 +0700 Subject: [PATCH 1/2] ci(cla): auto-record portal signatures + label server-side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Portal signature issues could never be processed into .cla/signatures.json without a manual owner commit. New cla-sync workflow: on [CLA] Signature issue -> validate identity (issue opener must equal claimed GitHub user, matching the portal's OAuth auto-fill) -> append to signatures.json (idempotent re-sign) -> apply the cla-signature label server-side (contributors cannot create labels) -> open an auto-generated PR for owner approval -> comment + close. cla-check bot comment now tells contributors to wait for the green submit confirmation, so a silently failed submission is noticed immediately. Requires secret OWNER_PAT (contents:write on this repo) — org default workflow token is read-only. --- .github/workflows/cla-check.yml | 2 + .github/workflows/cla-sync.yml | 158 ++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 .github/workflows/cla-sync.yml diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index f0fa96a..378e1e8 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -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.', + '', '---', '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).', ].join('\n'); diff --git a/.github/workflows/cla-sync.yml b/.github/workflows/cla-sync.yml new file mode 100644 index 0000000..9ea8bec --- /dev/null +++ b/.github/workflows/cla-sync.yml @@ -0,0 +1,158 @@ +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 + with: + script: | + const result = '${{ steps.apply.outputs.result }}'; + const prUrl = '${{ steps.pr.outputs.pr_url }}'; + let msg; + 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: ${{ steps.apply.outputs.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' + run: gh issue close ${{ github.event.issue.number }} --repo codecoradev/.github --reason completed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b73d15292fa0a2803a60bc5245effd35fd23ab18 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Thu, 10 Sep 2026 21:01:42 +0700 Subject: [PATCH 2/2] fix(cla-sync): route issue-derived outputs through env (script injection) Cora review flagged CRITICAL: attacker-controlled outputs (issue body) interpolated via ${{ }} into github-script source. Read them through process.env instead. Also: don't close the issue when the PR step failed, so a failed submission stays open with the failure comment as the audit trail. --- .github/workflows/cla-sync.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cla-sync.yml b/.github/workflows/cla-sync.yml index 9ea8bec..cabcf08 100644 --- a/.github/workflows/cla-sync.yml +++ b/.github/workflows/cla-sync.yml @@ -132,17 +132,24 @@ jobs: - 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: | - const result = '${{ steps.apply.outputs.result }}'; - const prUrl = '${{ steps.pr.outputs.pr_url }}'; - let msg; + // 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: ${{ steps.apply.outputs.reason }}\n\nPlease re-submit via https://codecoradev.github.io/cla/ (make sure you sign in with GitHub first), or contact a maintainer.`; + 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, @@ -152,7 +159,7 @@ jobs: }); - name: Close processed issue - if: steps.apply.outputs.result == 'ok' + 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 }}