diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json new file mode 100644 index 0000000..f6a3ff8 --- /dev/null +++ b/.github/cla-signatures.json @@ -0,0 +1,12 @@ +{ + "claVersion": "1.0", + "signatures": [ + { + "name": "Jegors Čemisovs", + "github": "rabestro", + "email": "jegors.cemisovs@gmail.com", + "claVersion": "1.0", + "date": "2026-08-18" + } + ] +} diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml index 8d48508..ebb78ba 100644 --- a/.github/workflows/cla.yaml +++ b/.github/workflows/cla.yaml @@ -1,29 +1,120 @@ -name: "CLA Assistant" +name: "CI: CLA" +# pull_request_target (not pull_request) is deliberate: the workflow LOGIC then +# always comes from the base branch, so a pull request cannot edit this file to +# neuter the check. The signature registry is still read from the PR head via +# the API below — it is pure data (parsed as JSON, never executed), and reading +# it from the untrusted side is exactly the point: signing happens in the +# contributor's first pull request. No code from the PR is checked out or run. +# +# NOTE: a pull_request_target workflow change cannot be verified on its own PR — +# the workflow always comes from the base branch, so the corrected version first +# executes on the next pull request after this change merges into main. on: - issue_comment: - types: [created] pull_request_target: - types: [opened, closed, synchronize] + types: [opened, synchronize, reopened] permissions: - actions: write - contents: write - pull-requests: write - statuses: write + contents: read jobs: - cla: + cla-check: runs-on: ubuntu-latest - 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' steps: - - name: "CLA Assistant" - uses: contributor-assistant/github-action@v2.6.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} + - name: Verify the PR author has signed the CLA + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - path-to-signatures: "cla-signatures/version-1/signatures.json" - path-to-document: "https://github.com/fortemate/.github/blob/main/CLA.md" - branch: "main" - allowlist: "dependabot[bot],greenkeeper[bot],rabestro,renovate[bot]" + script: | + const author = context.payload.pull_request.user.login; + if (author.endsWith('[bot]')) { + console.log(`@${author} is a bot account — CLA not required.`); + return; + } + // Insiders are exempt. OWNER covers a user-owned repository, MEMBER + // an organization member, COLLABORATOR anyone explicitly granted + // access to this repository. Deliberately NOT a comparison against + // context.repo.owner — that breaks the moment a repository moves + // into an organization, because the owner is then the org login and + // never the maintainer's. + // + // COLLABORATOR is in the list on purpose: when organization + // membership is concealed (private), the webhook payload does not + // report MEMBER, and the maintainer's own pull requests were failing + // this check. None of the three can be self-assigned by an outside + // contributor, so the gate still holds. + const association = context.payload.pull_request.author_association; + if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association)) { + console.log(`@${author} is ${association} — CLA not required.`); + return; + } + console.log(`@${author} is ${association} — a CLA signature is required.`); + const file = '.github/cla-signatures.json'; + const readRegistry = async (ref) => { + const { data } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: file, + ref, + }); + return JSON.parse(Buffer.from(data.content, 'base64').toString('utf8')); + }; + // The authoritative agreement version comes from the BASE BRANCH, + // not from the PR head: reading it from the head would let a + // contributor invent a version — or delete the field entirely, in + // which case an entry without claVersion matches through + // undefined === undefined and the check passes with no signature + // under the real agreement. + // + // Read by branch ref rather than base.sha on purpose. base.sha is + // the commit the pull request was opened against, which for an + // older open PR can predate the registry file entirely — that made + // this check fail with a 404. The agreement in force is the one on + // the target branch now, and a contributor cannot modify it. + let current; + try { + const base = await readRegistry(context.payload.pull_request.base.ref); + current = base.claVersion; + } catch (error) { + core.setFailed(`Cannot read ${file} from the base branch: ${error.message}`); + return; + } + if (typeof current !== 'string' || current.length === 0) { + core.setFailed(`${file} on the base branch has no usable "claVersion" — cannot verify signatures.`); + return; + } + // Signatures, in contrast, are read from the PR head on purpose: + // an entry added in this same pull request counts, so signing is + // part of the first contribution. (A pull request that bumps the + // agreement version is a maintainer change, and maintainers are + // exempt above, so the base/head version skew does not bite.) + let registry; + try { + registry = await readRegistry(`refs/pull/${context.payload.pull_request.number}/head`); + } catch (error) { + core.setFailed(`Cannot read ${file} from the PR head: ${error.message}`); + return; + } + if (!registry || !Array.isArray(registry.signatures)) { + core.setFailed(`${file} on the PR head must contain a "signatures" array.`); + return; + } + const entries = registry.signatures.filter( + (entry) => entry && (entry.github ?? '').toLowerCase() === author.toLowerCase() + ); + if (entries.some((entry) => entry.claVersion === current)) { + console.log(`@${author} has signed CLA v${current}.`); + return; + } + if (entries.length > 0) { + core.setFailed( + `@${author} signed an earlier CLA version, but the current agreement is v${current}. ` + + `Please re-read CLA.md and update your entry in ${file} to "claVersion": "${current}".` + ); + return; + } + core.setFailed( + `@${author} has not signed the Contributor License Agreement. ` + + `Please read CLA.md and add yourself to ${file} in this pull request ` + + `(with "claVersion": "${current}") — the commit adding your entry is ` + + `your electronic signature.` + ); diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..82cd242 --- /dev/null +++ b/CLA.md @@ -0,0 +1,102 @@ +# Individual Contributor License Agreement + +**Version 1.0** + +Thank you for your interest in contributing to the Dice Chess project ("the Project"), +maintained by Fortemate / Jegors Čemisovs ("the Project Owner", [github.com/fortemate](https://github.com/fortemate)). + +This Contributor License Agreement ("Agreement") documents the rights You grant to the +Project Owner for Your Contributions to the repository in which this file is stored. +It protects You as a contributor as well as the Project Owner; it does not change Your +rights to use Your own Contributions for any other purpose. Please read it carefully +before signing. + +## 1. Definitions + +- **"You"** (or **"Your"**) means the individual who Submits a Contribution to the Project. +- **"Contribution"** means any original work of authorship — source code, documentation, + configuration, test data, or other material — that You Submit to the Project. +- **"Submit"** means any form of electronic communication sent to the Project or its + maintainers, including pull requests, patches, and issue attachments, but excluding + communication that You conspicuously mark "Not a Contribution". + +## 2. Grant of Copyright License + +You retain ownership of the copyright in Your Contribution. + +Subject to the terms of this Agreement, You grant the Project Owner a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce Your Contribution, prepare derivative works of it, publicly display it, +publicly perform it, sublicense it, and distribute it and such derivative works. + +This license expressly includes the right to license and relicense Your Contribution, +in whole or in part, under any license terms the Project Owner chooses — including +copyleft, permissive, and proprietary or commercial licenses. + +*Plain-language note (not a limitation of the grant above): the Project follows an +open-core model. The public repositories remain available under their published +open-source licenses, and Your Contribution always stays available under the +repository's open-source license; this clause additionally preserves the Project +Owner's ability to offer the Project under other terms, such as combining it with +closed-source modules or commercial offerings.* + +## 3. Grant of Patent License + +Subject to the terms of this Agreement, You grant the Project Owner and recipients of +software distributed by the Project Owner a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent license +to make, have made, use, offer to sell, sell, import, and otherwise transfer the work +to which Your Contribution belongs, where such license applies only to those patent +claims licensable by You that are necessarily infringed by Your Contribution alone or +by combination of Your Contribution with the work to which it was Submitted. If any +entity institutes patent litigation against You or any other entity alleging that Your +Contribution, or the work to which You contributed, constitutes direct or contributory +patent infringement, then any patent licenses granted to that entity under this +Agreement for that Contribution or work terminate as of the date such litigation is +filed. + +## 4. Your Representations + +You represent that: + +1. You are legally entitled to grant the licenses above. +2. Each of Your Contributions is Your original creation. +3. If Your employer has rights to intellectual property that You create — which may + include Your Contribution — You have received permission to make the Contribution + on behalf of that employer, or Your employer has waived such rights for the + Contribution. +4. If Your Contribution includes work that is not Your original creation, You will + Submit it with complete details of its source and of any license or other + restriction of which You are aware, conspicuously marked as third-party material. + +You agree to notify the Project Owner if You become aware of any facts that would make +these representations inaccurate. + +## 5. No Obligation and No Warranty + +You are not expected to provide support for Your Contribution, except to the extent +You desire to provide it. Unless required by applicable law or agreed to in writing, +Your Contribution is provided "AS IS", without warranties or conditions of any kind. +The Project Owner is under no obligation to accept, use, or retain any Contribution. + +## 6. How to Sign + +Signing is self-service and happens in Your first pull request: + +1. Read this Agreement. +2. Append an entry for yourself to the `signatures` array in + [`.github/cla-signatures.json`](.github/cla-signatures.json) in the same pull request as your first contribution: + + ```json + { "github": "your-github-username", "name": "Your Full Name", "date": "YYYY-MM-DD", "claVersion": "1.0" } + ``` + +3. The commit adding your entry constitutes your electronic signature of this + Agreement, and the git history serves as the signature record. + +The `claVersion` field records which version of this Agreement you signed. If the +Agreement is revised, the registry's top-level version is bumped, and you will be +asked to re-sign (add an updated entry) before your next contribution is accepted. + +The `CI: CLA` status check verifies the entry automatically and will fail the pull +request until a signature matching the current Agreement version is present. diff --git a/cla-signatures/version-1/signatures.json b/cla-signatures/version-1/signatures.json deleted file mode 100644 index 18d5487..0000000 --- a/cla-signatures/version-1/signatures.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "signedContributors": [] -} \ No newline at end of file