diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 6b1a438db..fdc1df52c 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -4,7 +4,7 @@ on: issue_comment: types: [created] pull_request_target: - types: [opened, closed, synchronize] + types: [opened, synchronize] permissions: actions: write @@ -16,14 +16,128 @@ jobs: cla: runs-on: ubuntu-latest steps: + - name: Check stored CLA signatures + id: cla_state + if: github.event_name == 'pull_request_target' + uses: actions/github-script@v7 + with: + script: | + const signaturePath = "signatures/cla.json"; + const signatureBranch = "dev"; + const allowlist = [ + "code-yeongyu", + "bot*", + "dependabot*", + "github-actions*", + "*[bot]", + "sisyphus-dev-ai", + "web-flow", + ]; + + const matchesAllowlist = (login) => { + const normalizedLogin = login.toLowerCase(); + + return allowlist.some((entry) => { + const pattern = entry.toLowerCase(); + + if (pattern.startsWith("*")) { + return normalizedLogin.endsWith(pattern.slice(1)); + } + + if (pattern.endsWith("*")) { + return normalizedLogin.startsWith(pattern.slice(0, -1)); + } + + return normalizedLogin === pattern; + }); + }; + + const { owner, repo } = context.repo; + const pullNumber = context.payload.pull_request.number; + + const [signatureFile, commits] = await Promise.all([ + github.rest.repos.getContent({ + owner, + repo, + path: signaturePath, + ref: signatureBranch, + }), + github.paginate(github.rest.pulls.listCommits, { + owner, + repo, + pull_number: pullNumber, + per_page: 100, + }), + ]); + + if (Array.isArray(signatureFile.data) || signatureFile.data.type !== "file") { + core.setFailed(`${signaturePath} is not a file on ${signatureBranch}`); + return; + } + + const signatureContent = Buffer.from( + signatureFile.data.content, + signatureFile.data.encoding, + ).toString("utf8"); + const signatures = JSON.parse(signatureContent); + const signedContributors = Array.isArray(signatures.signedContributors) + ? signatures.signedContributors + : []; + const signedIds = new Set( + signedContributors + .map((contributor) => Number(contributor.id)) + .filter((id) => Number.isFinite(id)), + ); + const signedNames = new Set( + signedContributors + .map((contributor) => String(contributor.name || "").toLowerCase()) + .filter(Boolean), + ); + const contributors = new Map(); + + for (const commit of commits) { + if (commit.author?.login && commit.author?.id) { + contributors.set(commit.author.login, { + id: commit.author.id, + login: commit.author.login, + }); + } + } + + const unsigned = [...contributors.values()].filter((contributor) => { + const login = contributor.login.toLowerCase(); + + return ( + !matchesAllowlist(contributor.login) && + !signedIds.has(contributor.id) && + !signedNames.has(login) + ); + }); + + core.setOutput("needs_cla_action", unsigned.length > 0 ? "true" : "false"); + + if (unsigned.length > 0) { + core.info( + `CLA Assistant needed for unsigned contributors: ${unsigned + .map((contributor) => contributor.login) + .join(", ")}`, + ); + } else { + core.info("All linked commit authors already signed the CLA or are allowlisted."); + } + - name: CLA Assistant - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + if: >- + (github.event_name == 'issue_comment' && + (github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA')) || + steps.cla_state.outputs.needs_cla_action == 'true' uses: contributor-assistant/github-action@v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: path-to-signatures: 'signatures/cla.json' - path-to-document: 'https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md' + path-to-document: 'https://github.com/code-yeongyu/oh-my-openagent/blob/dev/CLA.md' branch: 'dev' allowlist: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai,web-flow custom-notsigned-prcomment: |