├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── add-contributors-in-readme.yaml │ ├── assign-to-project.yaml │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .idea └── workspace.xml ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __tests__ ├── main.test.ts └── pullRequestLock.test.ts ├── action.yml ├── dist └── index.js ├── docs └── contributors.md ├── images ├── adding-clafile.gif ├── allowlist.gif ├── personal-access-token.gif ├── personalaccesstoken.gif ├── signature-process.gif └── signature-storage-file.gif ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── addEmptyCommit.ts ├── checkAllowList.ts ├── graphql.ts ├── interfaces.ts ├── main.ts ├── octokit.ts ├── persistence │ └── persistence.ts ├── pullRerunRunner.ts ├── pullrequest │ ├── pullRequestComment.ts │ ├── pullRequestCommentContent.ts │ ├── pullRequestLock.ts │ └── signatureComment.ts ├── setupClaCheck.ts └── shared │ ├── getInputs.ts │ └── pr-sign-comment.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ibakshay 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: 'Please describe your feature request below:' 4 | title: "[Feature]" 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/add-contributors-in-readme.yaml: -------------------------------------------------------------------------------- 1 | name: Add Contributors to readme file 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | contrib-readme-job: 10 | runs-on: ubuntu-latest 11 | name: A job to automate contrib in readme 12 | steps: 13 | - name: Contribute List 14 | uses: akhilmhdh/contributors-readme-action@v2.3.6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/assign-to-project.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Assign to Project(s) 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | env: 7 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 8 | 9 | jobs: 10 | assign_one_project: 11 | runs-on: ubuntu-latest 12 | name: Assign to One Project 13 | steps: 14 | - name: Assign NEW issues and NEW pull requests to project 2 15 | uses: srggrs/assign-one-project-github-action@1.2.1 16 | if: github.event.action == 'opened' 17 | with: 18 | project: 'https://github.com/cla-assistant/github-action/projects/2' 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master, branchprotection ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '40 10 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | steps: 20 | - name: Initialize Energy Estimation 21 | uses: green-coding-berlin/eco-ci-energy-estimation@v1 22 | with: 23 | task: start-measurement 24 | - name: "Checkout repository" 25 | uses: actions/checkout@v4 26 | - name: Checkout Repo Measurement 27 | uses: green-coding-berlin/eco-ci-energy-estimation@v1 28 | with: 29 | task: get-measurement 30 | label: 'repository checkout' 31 | - name: Use Node.js ${{ matrix.node-version }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | - name: Npm install 36 | run: npm ci 37 | - name: Npm build 38 | run: npm run build --if-present 39 | - name: Checkout Repo Measurement 40 | uses: green-coding-berlin/eco-ci-energy-estimation@v1 41 | with: 42 | task: get-measurement 43 | label: 'Npm activities' 44 | - name: Show Energy Results 45 | uses: green-coding-berlin/eco-ci-energy-estimation@v1 46 | with: 47 | task: display-results 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __tests__/runner/* 2 | .vscode 3 | node_modules 4 | lib 5 | .idea 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 1656580846726 88 | 98 | 99 | 1656581723823 100 | 105 | 106 | 1656584093833 107 | 112 | 113 | 1656584277180 114 | 119 | 122 | 123 | 125 | 126 | 135 | 137 | 138 | 139 | 140 | 141 | 143 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at akshay.iyyadurai.balasundaram@sap.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CLA Assistant 2 | 3 | You want to contribute to CLA Assistant? Welcome! Please read this document to understand what you can do: 4 | * [Help Others](#help-others) 5 | * [Analyze Issues](#analyze-issues) 6 | * [Report an Issue](#report-an-issue) 7 | * [Contribute Code](#contribute-code) 8 | 9 | ## Help Others 10 | 11 | You can help CLA Assistant by helping others who use it and need support. 12 | 13 | ## Analyze Issues 14 | 15 | Analyzing issue reports can be a lot of effort. Any help is welcome! 16 | Go to [the GitHub issue tracker](https://github.com/cla-assistant/cla-assistant/issues?state=open) and find an open issue which needs additional work or a bugfix (e.g. issues labeled with "help wanted" or "bug"). 17 | 18 | Additional work could include any further information, or a gist, or it might be a hint that helps understanding the issue. Maybe you can even find and [contribute](#contribute-code) a bugfix? 19 | 20 | ## Report an Issue 21 | 22 | If you find a bug - behavior of CLA Assistant code contradicting your expectation - you are welcome to report it. 23 | We can only handle well-reported, actual bugs, so please follow the guidelines below. 24 | 25 | Once you have familiarized with the guidelines, you can go to the [GitHub issue tracker for CLA Assistant](https://github.com/cla-assistant/cla-assistant/issues/new) to report the issue. 26 | 27 | ### Quick Checklist for Bug Reports 28 | 29 | Issue report checklist: 30 | * Real, current bug 31 | * No duplicate 32 | * Reproducible 33 | * Good summary 34 | * Well-documented 35 | * Minimal example 36 | * Use the [template](ISSUE_TEMPLATE.md) 37 | 38 | 39 | ### Issue handling process 40 | 41 | When an issue is reported, a committer will look at it and either confirm it as a real issue, close it if it is not an issue, or ask for more details. 42 | 43 | An issue that is about a real bug is closed as soon as the fix is committed. 44 | 45 | 46 | ### Reporting Security Issues 47 | 48 | If you find a security issue, please act responsibly and report it not in the public issue tracker, but directly to us, so we can fix it before it can be exploited. 49 | Please send the related information to secure@sap.com using [PGP for e-mail encryption](https://global.sap.com/pc/security/keyblock.txt). 50 | Also refer to the general [SAP security information page](https://www.sap.com/corporate/en/company/security.html). 51 | 52 | 53 | ### Usage of Labels 54 | 55 | GitHub offers labels to categorize issues. We defined the following labels so far: 56 | 57 | Labels for issue categories: 58 | * bug: this issue is a bug in the code 59 | * feature: this issue is a request for a new functionality or an enhancement request 60 | * design: this issue relates to the UI or UX design of the tool 61 | 62 | Status of open issues: 63 | * help wanted: the feature request is approved and you are invited to contribute 64 | 65 | Status/resolution of closed issues: 66 | * wontfix: while acknowledged to be an issue, a fix cannot or will not be provided 67 | 68 | The labels can only be set and modified by committers. 69 | 70 | 71 | ### Issue Reporting Disclaimer 72 | 73 | We want to improve the quality of CLA Assistant and good bug reports are welcome! But our capacity is limited, thus we reserve the right to close or to not process insufficient bug reports in favor of those which are very cleanly documented and easy to reproduce. Even though we would like to solve each well-documented issue, there is always the chance that it will not happen - remember: CLA Assistant is Open Source and comes without warranty. 74 | 75 | Bug report analysis support is very welcome! (e.g. pre-analysis or proposing solutions) 76 | 77 | 78 | ## Contribute Code 79 | 80 | You are welcome to contribute code to CLA Assistant in order to fix bugs or to implement new features. 81 | 82 | There are three important things to know: 83 | 84 | 1. You must be aware that you need to submit [Developer Certificate of Origin](https://developercertificate.org/) in order for your contribution to be accepted. This is common practice in all major Open Source projects. 85 | 2. There are **several requirements regarding code style, quality, and product standards** which need to be met (we also have to follow them). The respective section below gives more details on the coding guidelines. 86 | 3. **Not all proposed contributions can be accepted**. Some features may e.g. just fit a third-party add-on better. The code must fit the overall direction of CLA Assistant and really improve it. The more effort you invest, the better you should clarify in advance whether the contribution fits: the best way would be to just open an issue to discuss the feature you plan to implement (make it clear you intend to contribute). 87 | 88 | ## Developer Certificate of Origin (DCO) 89 | 90 | Due to legal reasons, contributors will be asked to accept a DCO before they submit the first pull request to this projects, this happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). 91 | 92 | ### Contribution Content Guidelines 93 | 94 | These are some of the rules we try to follow: 95 | 96 | - Apply a clean coding style adapted to the surrounding code, even though we are aware the existing code is not fully clean 97 | - Use (4)spaces for indentation (except if the modified file consistently uses tabs) 98 | - Use variable naming conventions like in the other files you are seeing (camelcase) 99 | - No console.log() - use logging service 100 | - Run the ESLint code check and make it succeed 101 | - Comment your code where it gets non-trivial 102 | - Keep an eye on performance and memory consumption, properly destroy objects when not used anymore 103 | - Write a unit test 104 | - Do not do any incompatible changes, especially do not modify the name or behavior of public API methods or properties 105 | 106 | ### How to contribute - the Process 107 | 108 | 1. Make sure the change would be welcome (e.g. a bugfix or a useful feature); best do so by proposing it in a GitHub issue 109 | 2. Create a branch forking the cla-assistant repository and do your change 110 | 3. Commit and push your changes on that branch 111 | 4. In the commit message 112 | - Describe the problem you fix with this change. 113 | - Describe the effect that this change has from a user's point of view. App crashes and lockups are pretty convincing for example, but not all bugs are that obvious and should be mentioned in the text. 114 | - Describe the technical details of what you changed. It is important to describe the change in a most understandable way so the reviewer is able to verify that the code is behaving as you intend it to. 115 | 5. If your change fixes an issue reported at GitHub, add the following line to the commit message: 116 | - ```Fixes #(issueNumber)``` 117 | - Do NOT add a colon after "Fixes" - this prevents automatic closing. 118 | 6. Create a Pull Request 119 | 7. Follow the link posted by the CLA assistant to your pull request and accept it, as described in detail above. 120 | 8. Wait for our code review and approval, possibly enhancing your change on request 121 | - Note that the CLA Assistant developers also have their regular duties, so depending on the required effort for reviewing, testing and clarification this may take a while 122 | 123 | 9. Once the change has been approved we will inform you in a comment 124 | 10. We will close the pull request, feel free to delete the now obsolete branch 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. 30 | 31 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. 34 | 35 | Subject to the terms and conditions of this License, each Contributor hereby grants to You 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, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 36 | 37 | 4. Redistribution. 38 | 39 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 40 | 41 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 42 | You must cause any modified files to carry prominent notices stating that You changed the files; and 43 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 44 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 45 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 46 | 47 | 5. Submission of Contributions. 48 | 49 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 50 | 51 | 6. Trademarks. 52 | 53 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 54 | 55 | 7. Disclaimer of Warranty. 56 | 57 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 58 | 59 | 8. Limitation of Liability. 60 | 61 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 62 | 63 | 9. Accepting Warranty or Additional Liability. 64 | 65 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 66 | 67 | END OF TERMS AND CONDITIONS 68 | 69 | APPENDIX: How to apply the Apache License to your work 70 | 71 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 72 | 73 | Copyright 2020 SAP SE 74 | 75 | Licensed under the Apache License, Version 2.0 (the "License"); 76 | you may not use this file except in compliance with the License. 77 | You may obtain a copy of the License at 78 | 79 | http://www.apache.org/licenses/LICENSE-2.0 80 | 81 | Unless required by applicable law or agreed to in writing, software 82 | distributed under the License is distributed on an "AS IS" BASIS, 83 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 84 | See the License for the specific language governing permissions and 85 | limitations under the License. 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://github.com/cla-assistant/github-action/workflows/build/badge.svg) 2 | 3 | # Handling CLAs and DCOs via GitHub Action 4 | 5 | Streamline your workflow and let this GitHub Action (a lite version of [CLA Assistant](https://github.com/cla-assistant/cla-assistant)) handle the legal side of contributions to a repository for you. CLA assistant GitHub action enables contributors to sign CLAs from within a pull request. With this GitHub Action we could get rid of the need for a centrally managed database by **storing the contributor's signature data** in a decentralized way - **in the same repository's file system** or **in a remote repository** which can be even a private repository. 6 | 7 | ### Features 8 | 1. decentralized data storage 9 | 1. fully integrated within github environment 10 | 1. no User Interface is required 11 | 1. contributors can sign the CLA or DCO by just posting a Pull Request comment 12 | 1. signatures will be stored in a file inside the repository or in a remote repository 13 | 1. signatures can also be stored inside a private repository 14 | 1. versioning of signatures 15 | 16 | ## Configure Contributor License Agreement within two minutes 17 | 18 | #### 1. Add the following Workflow File to your repository in this path`.github/workflows/cla.yml` 19 | 20 | ```yml 21 | name: "CLA Assistant" 22 | on: 23 | issue_comment: 24 | types: [created] 25 | pull_request_target: 26 | types: [opened,closed,synchronize] 27 | 28 | # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings 29 | permissions: 30 | actions: write 31 | contents: write # this can be 'read' if the signatures are in remote repository 32 | pull-requests: write 33 | statuses: write 34 | 35 | jobs: 36 | CLAAssistant: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: "CLA Assistant" 40 | 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' 41 | uses: contributor-assistant/github-action@v2.6.1 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | # the below token should have repo scope and must be manually added by you in the repository's secret 45 | # This token is required only if you have configured to store the signatures in a remote repository/organization 46 | # PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 47 | with: 48 | path-to-signatures: 'signatures/version1/cla.json' 49 | path-to-document: 'https://github.com/cla-assistant/github-action/blob/master/SAPCLA.md' # e.g. a CLA or a DCO document 50 | # branch should not be protected 51 | branch: 'main' 52 | allowlist: user1,bot* 53 | 54 | # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken 55 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 56 | #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) 57 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 58 | #signed-commit-message: 'For example: $contributorName has signed the CLA in $owner/$repo#$pullRequestNo' 59 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 60 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 61 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 62 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 63 | #use-dco-flag: true - If you are using DCO instead of CLA 64 | 65 | ``` 66 | 67 | ##### Demo for step 1 68 | 69 | ![add-cla-file](https://github.com/cla-assistant/github-action/blob/master/images/adding-clafile.gif?raw=true) 70 | 71 | #### 2. Pull Request event triggers CLA Workflow 72 | 73 | CLA action workflow will be triggered on all Pull Request `opened, synchronize, closed`. This workflow will always run in the base repository and that's why we are making use of the [pull_request_target](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target) event. 74 |
When the CLA workflow is triggered on pull request `closed` event, it will lock the Pull Request conversation after the Pull Request merge so that the contributors cannot modify or delete the signatures (Pull Request comment) later. This feature is optional. 75 | 76 | #### 3. Signing the CLA 77 | 78 | CLA workflow creates a comment on Pull Request asking contributors who have not signed CLA to sign and also fails the pull request status check with a `failure`. The contributors are requested to sign the CLA within the pull request by copy and pasting **"I have read the CLA Document and I hereby sign the CLA"** as a Pull Request comment like below. 79 | If the contributor has already signed the CLA, then the PR status will pass with `success`.
80 | 81 | ##### Demo for step 2 and 3 82 | 83 | ![signature-process](https://github.com/cla-assistant/github-action/blob/master/images/signature-process.gif?raw=true) 84 | 85 |
86 | 87 | #### 4. Signatures stored in a JSON file 88 | 89 | After the contributor signed a CLA, the contributor's signature with metadata will be stored in a JSON file inside the repository and you can specify the custom path to this file with `path-to-signatures` input in the workflow.
The default path is `path-to-signatures: 'signatures/version1/cla.json'`. 90 | 91 | The signature can be also stored in a remote repository which can be done by enabling the optional inputs `remote-organization-name`: `` 92 | and `remote-repository-name`: `` in your CLA workflow file. 93 | 94 | **NOTE:** You do not need to create this file manually. Our workflow will create the signature file if it does not already exist. Manually creating this file will cause the workflow to fail. 95 | 96 | ##### Demo for step 4 97 | 98 | ![signature-storage-file](https://github.com/cla-assistant/github-action/blob/master/images/signature-storage-file.gif?raw=true) 99 | 100 | #### 5. Users and bots in allowlist 101 | 102 | If a GitHub username is included in the allowlist, they will not be required to sign a CLA. You can make use of this feature If you don't want your colleagues working in the same team/organisation to sign a CLA. And also, since there's no way for bot users (such as Dependabot or Greenkeeper) to sign a CLA, you may want to add them in `allowlist`. You can do so by adding their names in a comma separated string to the `allowlist` input in the CLA workflow file(in this case `dependabot[bot],greenkeeper[bot]`). You can also use wildcard symbol in case you want to allow all bot users something like `bot*`. 103 | 104 | ##### Demo for step 5 105 | 106 | ![allowlist](https://github.com/cla-assistant/github-action/blob/master/images/allowlist.gif?raw=true) 107 | 108 | #### 6. Adding Personal Access Token as a Secret 109 | 110 | You have to create a [Repository Secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) with the name `PERSONAL_ACCESS_TOKEN`. 111 | This PAT should have repo scope and is only required if you have configured to store the signatures in a remote repository/organization. 112 | 113 | ##### Demo for step 6 114 | 115 | ![personal-access-token](https://github.com/cla-assistant/github-action/blob/master/images/personal-access-token.gif?raw=true) 116 | 117 | ### Environmental Variables: 118 | 119 | 120 | | Name | Requirement | Description | 121 | | --------------------- | ----------- | ----------- | 122 | | `GITHUB_TOKEN` | _required_ | Usage: `GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}`, CLA Action uses this in-built GitHub token to make the API calls for interacting with GitHub. It is built into Github Actions and does not need to be manually specified in your secrets store. [More Info](https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token)| 123 | | `PERSONAL_ACCESS_TOKEN` | _required_ | Usage: `PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN}}`, you have to create a [Personal Access Token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with `repo scope` and store in the repository's [secrets](https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets). | 124 | 125 | ### Inputs Description: 126 | 127 | | Name | Requirement | Description | Example | 128 | | --------------------- | ----------- | ----------- | ------- | 129 | | `path-to-document` | _required_ | provide full URL `https://` to the document which shall be signed by the contributor(s) It can be any file e.g. inside the repository or it can be a gist. | https://github.com/cla-assistant/github-action/blob/master/SAPCLA.md | 130 | | `path-to-signatures` | _optional_ | Path to the JSON file where all the signatures of the contributors will be stored inside the repository. | signatures/version1/cla.json | 131 | | `branch` | _optional_ | Branch in which all the signatures of the contributors will be stored and Default branch is `master`. | master | 132 | | `allowlist` | _optional_ | You can specify users and bots to be [added in allowlist](https://github.com/cla-assistant/github-action#5-users-and-bots-in-allowlist). | user1,user2,bot* | 133 | | `remote-repository-name` | _optional_ | provide the remote repository name where all the signatures should be stored . | remote repository name | 134 | | `remote-organization-name` | _optional_ | provide the remote organization name where all the signatures should be stored. | remote organization name | 135 | | `create-file-commit-message` | _optional_ |Commit message when a new CLA file is created. | Creating file for storing CLA Signatures. | 136 | | `signed-commit-message` | _optional_ | Commit message when a new contributor signs the CLA in a Pull Request. | $contributorName has signed the CLA in $pullRequestNo | 137 | | `custom-notsigned-prcomment` | _optional_ | Introductory Pull Request comment to ask new contributors to sign. | Thank you for your contribution and please kindly read and sign our $pathToCLADocument | 138 | | `custom-pr-sign-comment` | _optional_ | The signature to be committed in order to sign the CLA. | I have read the Developer Terms Document and I hereby accept the Terms | 139 | | `custom-allsigned-prcomment` | _optional_ | pull request comment when everyone has signed | All Contributors have signed the CLA. | 140 | | `lock-pullrequest-aftermerge` | _optional_ | Boolean input for locking the pull request after merging. Default is set to `true`. It is highly recommended to lock the Pull Request after merging so that the Contributors won't be able to revoke their signature comments after merge | false | 141 | | `suggest-recheck` | _optional_ | Boolean input for indicating if the action's comment should suggest that users comment `recheck`. Default is set to `true`. | false | 142 | 143 | ## Contributors 144 | 145 | 146 | 147 | 148 | 155 | 162 | 169 | 176 | 183 | 190 | 191 | 198 | 205 | 212 | 219 | 226 | 233 | 234 | 241 | 248 | 255 | 262 | 269 | 276 | 277 | 284 | 291 | 298 | 305 | 312 | 319 | 320 | 327 | 334 | 341 | 348 | 355 | 362 | 363 | 370 |
149 | 150 | matbos 151 |
152 | Mateusz Boś 153 |
154 |
156 | 157 | michael-spengler 158 |
159 | Michael Spengler 160 |
161 |
163 | 164 | ibakshay 165 |
166 | Akshay Iyyadurai Balasundaram 167 |
168 |
170 | 171 | AnandChowdhary 172 |
173 | Anand Chowdhary 174 |
175 |
177 | 178 | kingthorin 179 |
180 | Rick M 181 |
182 |
184 | 185 | Writhe 186 |
187 | Filip Moroz 188 |
189 |
192 | 193 | mmv08 194 |
195 | Mikhail 196 |
197 |
199 | 200 | manifestinteractive 201 |
202 | Peter Schmalfeldt 203 |
204 |
206 | 207 | mattrosno 208 |
209 | Matt Rosno 210 |
211 |
213 | 214 | Or-Geva 215 |
216 | Or Geva 217 |
218 |
220 | 221 | pellared 222 |
223 | Robert Pająk 224 |
225 |
227 | 228 | ScottBrenner 229 |
230 | Scott Brenner 231 |
232 |
235 | 236 | silviogutierrez 237 |
238 | Silvio 239 |
240 |
242 | 243 | azzamsa 244 |
245 | Azzam S.A 246 |
247 |
249 | 250 | Tropicao 251 |
252 | Alexis Lothoré 253 |
254 |
256 | 257 | alohr51 258 |
259 | Andrew Lohr 260 |
261 |
263 | 264 | fishcharlie 265 |
266 | Charlie Fish 267 |
268 |
270 | 271 | darrellwarde 272 |
273 | Darrell Warde 274 |
275 |
278 | 279 | Holzhaus 280 |
281 | Jan Holthuis 282 |
283 |
285 | 286 | nwalters512 287 |
288 | Nathan Walters 289 |
290 |
292 | 293 | rokups 294 |
295 | Rokas Kupstys 296 |
297 |
299 | 300 | shunkakinoki 301 |
302 | Shun Kakinoki 303 |
304 |
306 | 307 | simonmeggle 308 |
309 | Simon Meggle 310 |
311 |
313 | 314 | t8 315 |
316 | Tate Berenbaum 317 |
318 |
321 | 322 | Krinkle 323 |
324 | Timo Tijhof 325 |
326 |
328 | 329 | AndrewGable 330 |
331 | Andrew Gable 332 |
333 |
335 | 336 | knanao 337 |
338 | Knanao 339 |
340 |
342 | 343 | tada5hi 344 |
345 | Peter 346 |
347 |
349 | 350 | wh201906 351 |
352 | Self Not Found 353 |
354 |
356 | 357 | woxiwangshunlibiye 358 |
359 | Woyaoshunlibiye 360 |
361 |
364 | 365 | yahavi 366 |
367 | Yahav Itzhak 368 |
369 |
371 | 372 | 373 | ## License 374 | 375 | Contributor License Agreement assistant 376 | 377 | Licensed under the Apache License, Version 2.0 (the "License"); 378 | you may not use this file except in compliance with the License. 379 | You may obtain a copy of the License at 380 | 381 | http://www.apache.org/licenses/LICENSE-2.0 382 | 383 | Unless required by applicable law or agreed to in writing, software 384 | distributed under the License is distributed on an "AS IS" BASIS, 385 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 386 | See the License for the specific language governing permissions and 387 | limitations under the License. 388 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Vulnerabilities 2 | 3 | The CLA Assistant is built with security and data privacy in mind to ensure your data is safe. 4 | 5 | ## Reporting 6 | 7 | We are grateful for security researchers and users reporting a vulnerability to us, first. To ensure that your request is handled in a timely manner and non-disclosure of vulnerabilities can be assured, please follow the below guideline. 8 | 9 | **Please do not report security vulnerabilities directly on GitHub. GitHub Issues can be publicly seen and therefore would result in a direct disclosure.** 10 | 11 | For reporting a vulnerability, please use the Vulnerability Report Form for Security Researchers on [SAP Trust Center](https://www.sap.com/about/trust-center/security/incident-management.html). 12 | Please address questions about data privacy, security concepts, and other media requests using the Vulnerability Report Form for Security Researchers on SAP Trust Center. 13 | 14 | 15 | 16 | ## Disclosure Handling 17 | 18 | SAP is committed to timely review and respond to your request. The resolution of code defects will be handled by a dedicated group of security experts and prepared in a private GitHub repository. The project will inform the public about resolved security vulnerabilities via GitHub Security Advisories. 19 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import { context } from '@actions/github' 4 | import { getclas } from '../src/checkcla' 5 | import { lockPullRequest } from '../src/pullRequestLock' 6 | import { run } from '../src/main' 7 | import { mocked } from 'ts-jest/utils' 8 | 9 | jest.mock('@actions/core') 10 | jest.mock('@actions/github') 11 | jest.mock('../src/pullRequestLock') 12 | jest.mock('../src/checkcla') 13 | const mockedGetClas = mocked(getclas) 14 | const mockedLockPullRequest = mocked(lockPullRequest) 15 | 16 | 17 | describe('Pull request event', () => { 18 | 19 | beforeEach(async () => { 20 | // @ts-ignore 21 | github.context = { 22 | eventName: 'pull_request', 23 | ref: 'refs/pull/232/merge', 24 | workflow: 'CLA Assistant', 25 | action: 'ibakshaygithub-action-1', 26 | actor: 'ibakshay', 27 | payload: { 28 | action: 'closed', 29 | number: '1', 30 | pull_request: { 31 | number: 1, 32 | title: 'test', 33 | user: { 34 | login: 'ibakshay', 35 | }, 36 | }, 37 | repository: { 38 | name: 'auto-assign', 39 | owner: { 40 | login: 'ibakshay', 41 | }, 42 | }, 43 | }, 44 | repo: { 45 | owner: 'ibakshay', 46 | repo: 'auto-assign', 47 | }, 48 | issue: { 49 | owner: 'kentaro-m', 50 | repo: 'auto-assign', 51 | number: 1, 52 | }, 53 | sha: '' 54 | } 55 | 56 | } 57 | ) 58 | 59 | test('the lockPullRequest method should be called if there is a pull request merge/closed', async () => { 60 | 61 | await run() 62 | expect(mockedLockPullRequest).toHaveBeenCalled() 63 | 64 | 65 | }) 66 | 67 | test('the checkcla method should not called if there is a pull request merge/closed', async () => { 68 | 69 | await run() 70 | expect(mockedGetClas).not.toHaveBeenCalled() 71 | }) 72 | 73 | test('the lockPullRequest method should not be called if there is a pull request opened', async () => { 74 | 75 | github.context.payload.action = 'opened' 76 | await run() 77 | 78 | expect(mockedLockPullRequest).not.toHaveBeenCalled() 79 | 80 | }) 81 | 82 | test('the checkcla method should be called if there is a pull request opened', async () => { 83 | 84 | github.context.payload.action = 'opened' 85 | await run() 86 | expect(mockedGetClas).toHaveBeenCalled() 87 | 88 | }) 89 | 90 | test('the lockPullRequest method should not be called if there is a pull request sync', async () => { 91 | 92 | github.context.payload.action = 'synchronize' 93 | 94 | await run() 95 | 96 | expect(mockedLockPullRequest).not.toHaveBeenCalled() 97 | 98 | }) 99 | 100 | test('the checkcla method should be called if there is a pull request sync', async () => { 101 | github.context.payload.action = 'synchronize' 102 | await run() 103 | expect(mockedGetClas).toHaveBeenCalled() 104 | 105 | }) 106 | 107 | 108 | }) -------------------------------------------------------------------------------- /__tests__/pullRequestLock.test.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import { context } from '@actions/github' 4 | import { getclas } from '../src/checkcla' 5 | import { lockPullRequest } from '../src/pullRequestLock' 6 | import { run } from '../src/main' 7 | import { mocked } from 'ts-jest/utils' 8 | 9 | jest.mock('@actions/core') 10 | jest.mock('@actions/github') 11 | 12 | //const mockedLockPullRequest = mocked(lockPullRequest) -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "CLA assistant lite" 2 | description: "An action to handle the Contributor License Agreement (CLA) and Developer Certificate of Orgin (DCO)" 3 | author: "SAP" 4 | branding: 5 | icon: "award" 6 | color: blue 7 | inputs: 8 | path-to-signatures: 9 | description: "Give a path for storing CLAs in a json file " 10 | default: "./signatures/cla.json" 11 | branch: 12 | description: "provide a branch where all the CLAs are stored" 13 | default: "master" 14 | allowlist: 15 | description: "users in the allow list don't have to sign the CLA document" 16 | default: "" 17 | remote-repository-name: 18 | description: "provide the remote repository name where all the signatures should be stored" 19 | remote-organization-name: 20 | description: "provide the remote organization name where all the signatures should be stored" 21 | path-to-document: 22 | description: "Fully qualified web link to the document - example: https://github.com/cla-assistant/github-action/blob/master/SAPCLA.md" 23 | signed-commit-message: 24 | description: "Commit message when a new contributor signs the CLA in a PR" 25 | signed-empty-commit-message: 26 | description: "Commit message when a new contributor signs the CLA (empty)" 27 | create-file-commit-message: 28 | description: "Commit message when a new file is created" 29 | custom-notsigned-prcomment: 30 | description: "Introductory message to ask new contributors to sign" 31 | custom-pr-sign-comment: 32 | description: "The signature to be committed in order to sign the CLA." 33 | custom-allsigned-prcomment: 34 | description: "pull request comment when everyone has signed, defaults to **CLA Assistant Lite** All Contributors have signed the CLA." 35 | use-dco-flag: 36 | description: "Set this to true if you want to use a dco instead of a cla" 37 | default: "false" 38 | lock-pullrequest-aftermerge: 39 | description: "Will lock the pull request after merge so that the signature the contributors cannot revoke their signature comments after merge" 40 | default: "true" 41 | suggest-recheck: 42 | description: "Controls whether or not the action's comment should suggest that users comment `recheck`." 43 | default: "true" 44 | runs: 45 | using: "node20" 46 | main: 'dist/index.js' 47 | -------------------------------------------------------------------------------- /docs/contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | ### Checkin 4 | 5 | - Do checkin source (src) 6 | - Do checkin build output (lib) 7 | - Do checkin runtime node_modules 8 | - Do not checkin devDependency node_modules (husky can help see below) 9 | 10 | ### devDependencies 11 | 12 | In order to handle correctly checking in node_modules without devDependencies, we run [Husky](https://github.com/typicode/husky) before each commit. 13 | This step ensures that formatting and checkin rules are followed and that devDependencies are excluded. To make sure Husky runs correctly, please use the following workflow: 14 | 15 | ``` 16 | npm install # installs all devDependencies including Husky 17 | git add abc.ext # Add the files you've changed. This should include files in src, lib, and node_modules (see above) 18 | git commit -m "Informative commit message" # Commit. This will run Husky 19 | ``` 20 | 21 | During the commit step, Husky will take care of formatting all files with [Prettier](https://github.com/prettier/prettier) as well as pruning out devDependencies using `npm prune --production`. 22 | It will also make sure these changes are appropriately included in your commit (no further work is needed) -------------------------------------------------------------------------------- /images/adding-clafile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/adding-clafile.gif -------------------------------------------------------------------------------- /images/allowlist.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/allowlist.gif -------------------------------------------------------------------------------- /images/personal-access-token.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/personal-access-token.gif -------------------------------------------------------------------------------- /images/personalaccesstoken.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/personalaccesstoken.gif -------------------------------------------------------------------------------- /images/signature-process.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/signature-process.gif -------------------------------------------------------------------------------- /images/signature-storage-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contributor-assistant/github-action/ca4a40a7d1004f18d9960b404b97e5f30a505a08/images/signature-storage-file.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-action", 3 | "version": "0.0.1", 4 | "description": "GitHub Action for storing CLA signatures", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "test": "jest", 8 | "build": "tsc && ncc build", 9 | "buildAndAdd": "npm run build && git add ." 10 | }, 11 | "husky": { 12 | "hooks": { 13 | "pre-commit": "npm run buildAndAdd" 14 | } 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cla-assistant/github-action.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "node", 23 | "setup" 24 | ], 25 | "dependencies": { 26 | "@actions/core": "^1.10.0", 27 | "@actions/github": "^4.0.0", 28 | "@octokit/rest": "^16.43.2", 29 | "actions-toolkit": "^2.1.0", 30 | "husky": "^4.3.8", 31 | "lodash": "^4.17.21", 32 | "node-fetch": "^3.3.0" 33 | }, 34 | "devDependencies": { 35 | "@octokit/types": "8.1.1", 36 | "@types/node": "^18.11.18", 37 | "@vercel/ncc": "^0.38.0", 38 | "ts-jest": "^29.0.5", 39 | "typescript": "^4.9.4" 40 | }, 41 | "author": "ibakshay", 42 | "license": "Apache-2.0", 43 | "bugs": { 44 | "url": "https://github.com/cla-assistant/github-action/issues" 45 | }, 46 | "homepage": "https://github.com/cla-assistant/github-action#readme" 47 | } 48 | -------------------------------------------------------------------------------- /src/addEmptyCommit.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from './octokit' 2 | import { context } from '@actions/github' 3 | 4 | import * as core from '@actions/core' 5 | import * as input from './shared/getInputs' 6 | import { getPrSignComment } from './shared/pr-sign-comment' 7 | 8 | 9 | export async function addEmptyCommit() { 10 | const contributorName: string = context?.payload?.comment?.user?.login 11 | core.info(`Adding empty commit for ${contributorName} who has signed the CLA `) 12 | 13 | if (context.payload.comment) { 14 | 15 | //Do empty commit only when the contributor signs the CLA with the PR comment 16 | if (context.payload.comment.body.toLowerCase().trim() === getPrSignComment().toLowerCase().trim()) { 17 | try { 18 | const message = input.getSignedCommitMessage() ? 19 | input.getSignedCommitMessage().replace('$contributorName', contributorName) : 20 | ` @${contributorName} has signed the CLA ` 21 | const pullRequestResponse = await octokit.pulls.get({ 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | pull_number: context.payload.issue!.number 25 | }) 26 | 27 | const baseCommit = await octokit.git.getCommit({ 28 | owner: context.repo.owner, 29 | repo: context.repo.repo, 30 | commit_sha: pullRequestResponse.data.head.sha 31 | }) 32 | 33 | const tree = await octokit.git.getTree({ 34 | owner: context.repo.owner, 35 | repo: context.repo.repo, 36 | tree_sha: baseCommit.data.tree.sha 37 | }) 38 | const newCommit = await octokit.git.createCommit( 39 | { 40 | owner: context.repo.owner, 41 | repo: context.repo.repo, 42 | message: message, 43 | tree: tree.data.sha, 44 | parents: [pullRequestResponse.data.head.sha] 45 | } 46 | ) 47 | return octokit.git.updateRef({ 48 | owner: context.repo.owner, 49 | repo: context.repo.repo, 50 | ref: `heads/${pullRequestResponse.data.head.ref}`, 51 | sha: newCommit.data.sha 52 | }) 53 | 54 | } catch (error) { 55 | core.error(`failed when adding empty commit with the contributor's signature name: ${error} `) 56 | 57 | } 58 | } 59 | } 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /src/checkAllowList.ts: -------------------------------------------------------------------------------- 1 | import { CommittersDetails } from './interfaces' 2 | 3 | import * as _ from 'lodash' 4 | import * as input from './shared/getInputs' 5 | 6 | 7 | 8 | function isUserNotInAllowList(committer) { 9 | 10 | const allowListPatterns: string[] = input.getAllowListItem().split(',') 11 | 12 | return allowListPatterns.filter(function (pattern) { 13 | pattern = pattern.trim() 14 | if (pattern.includes('*')) { 15 | const regex = _.escapeRegExp(pattern).split('\\*').join('.*') 16 | 17 | return new RegExp(regex).test(committer) 18 | } 19 | return pattern === committer 20 | }).length > 0 21 | } 22 | 23 | export function checkAllowList(committers: CommittersDetails[]): CommittersDetails[] { 24 | const committersAfterAllowListCheck: CommittersDetails[] = committers.filter(committer => committer && !(isUserNotInAllowList !== undefined && isUserNotInAllowList(committer.name))) 25 | return committersAfterAllowListCheck 26 | } -------------------------------------------------------------------------------- /src/graphql.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from './octokit' 2 | import { context } from '@actions/github' 3 | import { CommittersDetails } from './interfaces' 4 | 5 | 6 | 7 | export default async function getCommitters(): Promise { 8 | try { 9 | let committers: CommittersDetails[] = [] 10 | let filteredCommitters: CommittersDetails[] = [] 11 | let response: any = await octokit.graphql(` 12 | query($owner:String! $name:String! $number:Int! $cursor:String!){ 13 | repository(owner: $owner, name: $name) { 14 | pullRequest(number: $number) { 15 | commits(first: 100, after: $cursor) { 16 | totalCount 17 | edges { 18 | node { 19 | commit { 20 | author { 21 | email 22 | name 23 | user { 24 | id 25 | databaseId 26 | login 27 | } 28 | } 29 | committer { 30 | name 31 | user { 32 | id 33 | databaseId 34 | login 35 | } 36 | } 37 | } 38 | } 39 | cursor 40 | } 41 | pageInfo { 42 | endCursor 43 | hasNextPage 44 | } 45 | } 46 | } 47 | } 48 | }`.replace(/ /g, ''), { 49 | owner: context.repo.owner, 50 | name: context.repo.repo, 51 | number: context.issue.number, 52 | cursor: '' 53 | }) 54 | response.repository.pullRequest.commits.edges.forEach(edge => { 55 | const committer = extractUserFromCommit(edge.node.commit) 56 | let user = { 57 | name: committer.login || committer.name, 58 | id: committer.databaseId || '', 59 | pullRequestNo: context.issue.number 60 | } 61 | if (committers.length === 0 || committers.map((c) => { 62 | return c.name 63 | }).indexOf(user.name) < 0) { 64 | committers.push(user) 65 | } 66 | }) 67 | filteredCommitters = committers.filter((committer) => { 68 | return committer.id !== 41898282 69 | }) 70 | return filteredCommitters 71 | 72 | } catch (e) { 73 | throw new Error(`graphql call to get the committers details failed: ${e}`) 74 | } 75 | 76 | } 77 | const extractUserFromCommit = (commit) => commit.author.user || commit.committer.user || commit.author || commit.committer -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface CommitterMap { 2 | signed: CommittersDetails[], 3 | notSigned: CommittersDetails[], 4 | unknown: CommittersDetails[] 5 | } 6 | export interface ReactedCommitterMap { 7 | newSigned: CommittersDetails[], 8 | onlyCommitters?: CommittersDetails[], 9 | allSignedFlag: boolean 10 | } 11 | export interface CommentedCommitterMap { 12 | newSigned: CommittersDetails[], 13 | onlyCommitters?: CommittersDetails[], 14 | allSignedFlag: boolean 15 | } 16 | export interface CommittersDetails { 17 | name: string, 18 | id: number, 19 | pullRequestNo?: number, 20 | created_at?: string, 21 | updated_at?: string 22 | comment_id?: number, 23 | body?: string, 24 | repoId?: string 25 | } 26 | export interface LabelName { 27 | current_name: string, 28 | name: string 29 | } 30 | export interface CommittersCommentDetails { 31 | name: string, 32 | id: number, 33 | comment_id: number, 34 | body: string, 35 | created_at: string, 36 | updated_at: string 37 | } 38 | export interface ClafileContentAndSha { 39 | claFileContent: any, 40 | sha: string 41 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {context} from '@actions/github' 2 | import {setupClaCheck} from './setupClaCheck' 3 | import {lockPullRequest} from './pullrequest/pullRequestLock' 4 | 5 | import * as core from '@actions/core' 6 | import * as input from './shared/getInputs' 7 | 8 | export async function run() { 9 | try { 10 | core.info(`CLA Assistant GitHub Action bot has started the process`) 11 | 12 | /* 13 | * using a `string` true or false purposely as github action input cannot have a boolean value 14 | */ 15 | if ( 16 | context.payload.action === 'closed' && 17 | input.lockPullRequestAfterMerge() == 'true' 18 | ) { 19 | return lockPullRequest() 20 | } else { 21 | await setupClaCheck() 22 | } 23 | } catch (error) { 24 | if (error instanceof Error) core.setFailed(error.message) 25 | } 26 | } 27 | 28 | run() 29 | -------------------------------------------------------------------------------- /src/octokit.ts: -------------------------------------------------------------------------------- 1 | import { getOctokit } from '@actions/github' 2 | 3 | import * as core from '@actions/core' 4 | 5 | const githubActionsDefaultToken = process.env.GITHUB_TOKEN 6 | const personalAccessToken = process.env.PERSONAL_ACCESS_TOKEN as string 7 | 8 | export const octokit = getOctokit(githubActionsDefaultToken as string) 9 | 10 | export function getDefaultOctokitClient() { 11 | return getOctokit(githubActionsDefaultToken as string) 12 | } 13 | export function getPATOctokit() { 14 | if (!isPersonalAccessTokenPresent()) { 15 | core.setFailed( 16 | `Please add a personal access token as an environment variable for writing signatures in a remote repository/organization as mentioned in the README.md file` 17 | ) 18 | } 19 | return getOctokit(personalAccessToken) 20 | } 21 | 22 | export function isPersonalAccessTokenPresent(): boolean { 23 | return personalAccessToken !== undefined && personalAccessToken !== '' 24 | } 25 | -------------------------------------------------------------------------------- /src/persistence/persistence.ts: -------------------------------------------------------------------------------- 1 | import { context } from '@actions/github' 2 | 3 | import { ReactedCommitterMap } from '../interfaces' 4 | import { GitHub } from '@actions/github/lib/utils' 5 | import { getDefaultOctokitClient, getPATOctokit } from '../octokit' 6 | 7 | import * as input from '../shared/getInputs' 8 | 9 | export async function getFileContent(): Promise { 10 | const octokitInstance: InstanceType = 11 | isRemoteRepoOrOrgConfigured() ? getPATOctokit() : getDefaultOctokitClient() 12 | 13 | const result = await octokitInstance.repos.getContent({ 14 | owner: input.getRemoteOrgName() || context.repo.owner, 15 | repo: input.getRemoteRepoName() || context.repo.repo, 16 | path: input.getPathToSignatures(), 17 | ref: input.getBranch() 18 | }) 19 | return result 20 | } 21 | 22 | export async function createFile(contentBinary): Promise { 23 | const octokitInstance: InstanceType = 24 | isRemoteRepoOrOrgConfigured() ? getPATOctokit() : getDefaultOctokitClient() 25 | 26 | return octokitInstance.repos.createOrUpdateFileContents({ 27 | owner: input.getRemoteOrgName() || context.repo.owner, 28 | repo: input.getRemoteRepoName() || context.repo.repo, 29 | path: input.getPathToSignatures(), 30 | message: 31 | input.getCreateFileCommitMessage() || 32 | 'Creating file for storing CLA Signatures', 33 | content: contentBinary, 34 | branch: input.getBranch() 35 | }) 36 | } 37 | 38 | export async function updateFile( 39 | sha: string, 40 | claFileContent, 41 | reactedCommitters: ReactedCommitterMap 42 | ): Promise { 43 | const octokitInstance: InstanceType = 44 | isRemoteRepoOrOrgConfigured() ? getPATOctokit() : getDefaultOctokitClient() 45 | 46 | const pullRequestNo = context.issue.number 47 | const owner = context.issue.owner 48 | const repo = context.issue.repo 49 | 50 | claFileContent?.signedContributors.push(...reactedCommitters.newSigned) 51 | let contentString = JSON.stringify(claFileContent, null, 2) 52 | let contentBinary = Buffer.from(contentString).toString('base64') 53 | await octokitInstance.repos.createOrUpdateFileContents({ 54 | owner: input.getRemoteOrgName() || context.repo.owner, 55 | repo: input.getRemoteRepoName() || context.repo.repo, 56 | path: input.getPathToSignatures(), 57 | sha, 58 | message: input.getSignedCommitMessage() 59 | ? input 60 | .getSignedCommitMessage() 61 | .replace('$contributorName', context.actor) 62 | // .replace('$pullRequestNo', pullRequestNo.toString()) 63 | .replace('$owner', owner) 64 | .replace('$repo', repo) 65 | : `@${context.actor} has signed the CLA in ${owner}/${repo}#${pullRequestNo}`, 66 | content: contentBinary, 67 | branch: input.getBranch() 68 | }) 69 | } 70 | 71 | function isRemoteRepoOrOrgConfigured(): boolean { 72 | let isRemoteRepoOrOrgConfigured = false 73 | if (input?.getRemoteRepoName() || input.getRemoteOrgName()) { 74 | isRemoteRepoOrOrgConfigured = true 75 | return isRemoteRepoOrOrgConfigured 76 | } 77 | return isRemoteRepoOrOrgConfigured 78 | } 79 | -------------------------------------------------------------------------------- /src/pullRerunRunner.ts: -------------------------------------------------------------------------------- 1 | import { context } from '@actions/github' 2 | import { octokit } from './octokit' 3 | 4 | import * as core from '@actions/core' 5 | 6 | // Note: why this re-run of the last failed CLA workflow status check is explained this issue https://github.com/cla-assistant/github-action/issues/39 7 | export async function reRunLastWorkFlowIfRequired() { 8 | if (context.eventName === 'pull_request') { 9 | core.debug(`rerun not required for event - pull_request`) 10 | return 11 | } 12 | 13 | const branch = await getBranchOfPullRequest() 14 | const workflowId = await getSelfWorkflowId() 15 | const runs = await listWorkflowRunsInBranch(branch, workflowId) 16 | 17 | if (runs.data.total_count > 0) { 18 | const run = runs.data.workflow_runs[0].id 19 | 20 | const isLastWorkFlowFailed: boolean = await checkIfLastWorkFlowFailed(run) 21 | if (isLastWorkFlowFailed) { 22 | core.debug(`Rerunning build run ${run}`) 23 | await reRunWorkflow(run).catch(error => 24 | core.error(`Error occurred when re-running the workflow: ${error}`) 25 | ) 26 | } 27 | } 28 | } 29 | 30 | async function getBranchOfPullRequest(): Promise { 31 | const pullRequest = await octokit.pulls.get({ 32 | owner: context.repo.owner, 33 | repo: context.repo.repo, 34 | pull_number: context.issue.number 35 | }) 36 | 37 | return pullRequest.data.head.ref 38 | } 39 | 40 | async function getSelfWorkflowId(): Promise { 41 | const perPage = 30 42 | let hasNextPage = true 43 | 44 | for (let page = 1; hasNextPage === true; page++) { 45 | const workflowList = await octokit.actions.listRepoWorkflows({ 46 | owner: context.repo.owner, 47 | repo: context.repo.repo, 48 | per_page: perPage, 49 | page 50 | }) 51 | 52 | if (workflowList.data.total_count < page * perPage) { 53 | hasNextPage = false 54 | } 55 | 56 | const workflow = workflowList.data.workflows.find( 57 | w => w.name == context.workflow 58 | ) 59 | 60 | if (workflow) { 61 | return workflow.id 62 | } 63 | } 64 | 65 | throw new Error( 66 | `Unable to locate this workflow's ID in this repository, can't trigger job..` 67 | ) 68 | } 69 | 70 | async function listWorkflowRunsInBranch( 71 | branch: string, 72 | workflowId: number 73 | ): Promise { 74 | console.debug(branch) 75 | const runs = await octokit.actions.listWorkflowRuns({ 76 | owner: context.repo.owner, 77 | repo: context.repo.repo, 78 | branch, 79 | workflow_id: workflowId, 80 | event: 'pull_request_target' 81 | }) 82 | return runs 83 | } 84 | 85 | async function reRunWorkflow(run: number): Promise { 86 | // Personal Access token with repo scope is required to access this api - https://github.community/t/bug-rerun-workflow-api-not-working/126742 87 | await octokit.actions.reRunWorkflow({ 88 | owner: context.repo.owner, 89 | repo: context.repo.repo, 90 | run_id: run 91 | }) 92 | } 93 | 94 | async function checkIfLastWorkFlowFailed(run: number): Promise { 95 | const response: any = await octokit.actions.getWorkflowRun({ 96 | owner: context.repo.owner, 97 | repo: context.repo.repo, 98 | run_id: run 99 | }) 100 | 101 | return response.data.conclusion == 'failure' 102 | } 103 | -------------------------------------------------------------------------------- /src/pullrequest/pullRequestComment.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from '../octokit' 2 | import { context } from '@actions/github' 3 | import signatureWithPRComment from './signatureComment' 4 | import { commentContent } from './pullRequestCommentContent' 5 | import { 6 | CommitterMap, 7 | CommittersDetails 8 | } from '../interfaces' 9 | import { getUseDcoFlag } from '../shared/getInputs' 10 | 11 | 12 | 13 | export default async function prCommentSetup(committerMap: CommitterMap, committers: CommittersDetails[]) { 14 | const signed = committerMap?.notSigned && committerMap?.notSigned.length === 0 15 | 16 | try { 17 | const claBotComment = await getComment() 18 | if (!claBotComment && !signed) { 19 | return createComment(signed, committerMap) 20 | } else if (claBotComment?.id) { 21 | if (signed) { 22 | await updateComment(signed, committerMap, claBotComment) 23 | } 24 | 25 | // reacted committers are contributors who have newly signed by posting the Pull Request comment 26 | const reactedCommitters = await signatureWithPRComment(committerMap, committers) 27 | if (reactedCommitters?.onlyCommitters) { 28 | reactedCommitters.allSignedFlag = prepareAllSignedCommitters(committerMap, reactedCommitters.onlyCommitters, committers) 29 | } 30 | committerMap = prepareCommiterMap(committerMap, reactedCommitters) 31 | await updateComment(reactedCommitters.allSignedFlag, committerMap, claBotComment) 32 | return reactedCommitters 33 | } 34 | } catch (error) { 35 | throw new Error( 36 | `Error occured when creating or editing the comments of the pull request: ${error.message}`) 37 | } 38 | } 39 | 40 | async function createComment(signed: boolean, committerMap: CommitterMap): Promise { 41 | await octokit.issues.createComment({ 42 | owner: context.repo.owner, 43 | repo: context.repo.repo, 44 | issue_number: context.issue.number, 45 | body: commentContent(signed, committerMap) 46 | }).catch(error => { throw new Error(`Error occured when creating a pull request comment: ${error.message}`) }) 47 | } 48 | 49 | async function updateComment(signed: boolean, committerMap: CommitterMap, claBotComment: any): Promise { 50 | await octokit.issues.updateComment({ 51 | owner: context.repo.owner, 52 | repo: context.repo.repo, 53 | comment_id: claBotComment.id, 54 | body: commentContent(signed, committerMap) 55 | }).catch(error => { throw new Error(`Error occured when updating the pull request comment: ${error.message}`) }) 56 | } 57 | 58 | async function getComment() { 59 | try { 60 | const response = await octokit.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }) 61 | 62 | //TODO: check the below regex 63 | // using a `string` true or false purposely as github action input cannot have a boolean value 64 | if (getUseDcoFlag() === 'true') { 65 | return response.data.find(comment => comment.body.match(/.*DCO Assistant Lite bot.*/m)) 66 | } else if (getUseDcoFlag() === 'false') { 67 | return response.data.find(comment => comment.body.match(/.*CLA Assistant Lite bot.*/m)) 68 | } 69 | } catch (error) { 70 | throw new Error(`Error occured when getting all the comments of the pull request: ${error.message}`) 71 | } 72 | } 73 | 74 | function prepareCommiterMap(committerMap: CommitterMap, reactedCommitters) { 75 | committerMap.signed?.push(...reactedCommitters.newSigned) 76 | committerMap.notSigned = committerMap.notSigned!.filter( 77 | committer => 78 | !reactedCommitters.newSigned.some( 79 | reactedCommitter => committer.id === reactedCommitter.id 80 | ) 81 | ) 82 | return committerMap 83 | 84 | } 85 | 86 | function prepareAllSignedCommitters(committerMap: CommitterMap, signedInPrCommitters: CommittersDetails[], committers: CommittersDetails[]): boolean { 87 | let allSignedCommitters = [] as CommittersDetails[] 88 | /* 89 | * 1) already signed committers in the file 2) signed committers in the PR comment 90 | */ 91 | const ids = new Set(signedInPrCommitters.map(committer => committer.id)) 92 | allSignedCommitters = [...signedInPrCommitters, ...committerMap.signed!.filter(signedCommitter => !ids.has(signedCommitter.id))] 93 | /* 94 | * checking if all the unsigned committers have reacted to the PR comment (this is needed for changing the content of the PR comment to "All committers have signed the CLA") 95 | */ 96 | let allSignedFlag: boolean = committers.every(committer => allSignedCommitters.some(reactedCommitter => committer.id === reactedCommitter.id)) 97 | return allSignedFlag 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/pullrequest/pullRequestCommentContent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommitterMap 3 | } from '../interfaces' 4 | import * as input from '../shared/getInputs' 5 | import { getPrSignComment } from '../shared/pr-sign-comment' 6 | 7 | export function commentContent(signed: boolean, committerMap: CommitterMap): string { 8 | // using a `string` true or false purposely as github action input cannot have a boolean value 9 | if (input.getUseDcoFlag() == 'true') { 10 | return dco(signed, committerMap) 11 | } else { 12 | return cla(signed, committerMap) 13 | } 14 | } 15 | 16 | function dco(signed: boolean, committerMap: CommitterMap): string { 17 | 18 | if (signed) { 19 | const line1 = input.getCustomAllSignedPrComment() || `All contributors have signed the DCO ✍️ ✅` 20 | const text = `${line1}
Posted by the ****DCO Assistant Lite bot****.` 21 | return text 22 | } 23 | let committersCount = 1 24 | 25 | if (committerMap && committerMap.signed && committerMap.notSigned) { 26 | committersCount = committerMap.signed.length + committerMap.notSigned.length 27 | 28 | } 29 | 30 | let you = committersCount > 1 ? `you all` : `you` 31 | let lineOne = (input.getCustomNotSignedPrComment() || `
Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that $you sign our [Developer Certificate of Origin](${input.getPathToDocument()}) before we can accept your contribution. You can sign the DCO by just posting a Pull Request Comment same as the below format.
`).replace('$you', you) 32 | let text = `${lineOne} 33 | - - - 34 | ${input.getCustomPrSignComment() || "I have read the DCO Document and I hereby sign the DCO"} 35 | - - - 36 | ` 37 | 38 | if (committersCount > 1 && committerMap && committerMap.signed && committerMap.notSigned) { 39 | text += `**${committerMap.signed.length}** out of **${committerMap.signed.length + committerMap.notSigned.length}** committers have signed the DCO.` 40 | committerMap.signed.forEach(signedCommitter => { text += `
:white_check_mark: (${signedCommitter.name})[https://github.com/${signedCommitter.name}]` }) 41 | committerMap.notSigned.forEach(unsignedCommitter => { 42 | text += `
:x: @${unsignedCommitter.name}` 43 | }) 44 | text += '
' 45 | } 46 | 47 | if (committerMap && committerMap.unknown && committerMap.unknown.length > 0) { 48 | let seem = committerMap.unknown.length > 1 ? "seem" : "seems" 49 | let committerNames = committerMap.unknown.map(committer => committer.name) 50 | text += `**${committerNames.join(", ")}** ${seem} not to be a GitHub user.` 51 | text += ' You need a GitHub account to be able to sign the DCO. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).
' 52 | } 53 | 54 | if (input.suggestRecheck() == 'true') { 55 | text += 'You can retrigger this bot by commenting **recheck** in this Pull Request. ' 56 | } 57 | text += 'Posted by the ****DCO Assistant Lite bot****.' 58 | return text 59 | } 60 | 61 | function cla(signed: boolean, committerMap: CommitterMap): string { 62 | 63 | if (signed) { 64 | const line1 = input.getCustomAllSignedPrComment() || `All contributors have signed the CLA ✍️ ✅` 65 | const text = `${line1}
Posted by the ****CLA Assistant Lite bot****.` 66 | return text 67 | } 68 | let committersCount = 1 69 | 70 | if (committerMap && committerMap.signed && committerMap.notSigned) { 71 | committersCount = committerMap.signed.length + committerMap.notSigned.length 72 | 73 | } 74 | 75 | let you = committersCount > 1 ? `you all` : `you` 76 | let lineOne = (input.getCustomNotSignedPrComment() || `
Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that $you sign our [Contributor License Agreement](${input.getPathToDocument()}) before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.
`).replace('$you', you) 77 | let text = `${lineOne} 78 | - - - 79 | ${getPrSignComment()} 80 | - - - 81 | ` 82 | 83 | if (committersCount > 1 && committerMap && committerMap.signed && committerMap.notSigned) { 84 | text += `**${committerMap.signed.length}** out of **${committerMap.signed.length + committerMap.notSigned.length}** committers have signed the CLA.` 85 | committerMap.signed.forEach(signedCommitter => { text += `
:white_check_mark: (${signedCommitter.name})[https://github.com/${signedCommitter.name}]` }) 86 | committerMap.notSigned.forEach(unsignedCommitter => { 87 | text += `
:x: @${unsignedCommitter.name}` 88 | }) 89 | text += '
' 90 | } 91 | 92 | if (committerMap && committerMap.unknown && committerMap.unknown.length > 0) { 93 | let seem = committerMap.unknown.length > 1 ? "seem" : "seems" 94 | let committerNames = committerMap.unknown.map(committer => committer.name) 95 | text += `**${committerNames.join(", ")}** ${seem} not to be a GitHub user.` 96 | text += ' You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).
' 97 | } 98 | 99 | if (input.suggestRecheck() == 'true') { 100 | text += 'You can retrigger this bot by commenting **recheck** in this Pull Request. ' 101 | } 102 | text += 'Posted by the **CLA Assistant Lite bot**.' 103 | return text 104 | } 105 | -------------------------------------------------------------------------------- /src/pullrequest/pullRequestLock.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from '../octokit' 2 | import * as core from '@actions/core' 3 | import { context } from '@actions/github' 4 | 5 | export async function lockPullRequest() { 6 | core.info('Locking the Pull Request to safe guard the Pull Request CLA Signatures') 7 | const pullRequestNo: number = context.issue.number 8 | try { 9 | await octokit.issues.lock( 10 | { 11 | owner: context.repo.owner, 12 | repo: context.repo.repo, 13 | issue_number: pullRequestNo 14 | } 15 | ) 16 | core.info(`successfully locked the pull request ${pullRequestNo}`) 17 | } catch (e) { 18 | core.error(`failed when locking the pull request `) 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/pullrequest/signatureComment.ts: -------------------------------------------------------------------------------- 1 | import { octokit } from '../octokit' 2 | import { context } from '@actions/github' 3 | import { CommitterMap, CommittersDetails, ReactedCommitterMap } from '../interfaces' 4 | import { getUseDcoFlag, getCustomPrSignComment } from '../shared/getInputs' 5 | 6 | import * as core from '@actions/core' 7 | 8 | export default async function signatureWithPRComment(committerMap: CommitterMap, committers): Promise { 9 | 10 | let repoId = context.payload.repository!.id 11 | let prResponse = await octokit.issues.listComments({ 12 | owner: context.repo.owner, 13 | repo: context.repo.repo, 14 | issue_number: context.issue.number 15 | }) 16 | let listOfPRComments = [] as CommittersDetails[] 17 | let filteredListOfPRComments = [] as CommittersDetails[] 18 | 19 | prResponse?.data.map((prComment) => { 20 | listOfPRComments.push({ 21 | name: prComment.user.login, 22 | id: prComment.user.id, 23 | comment_id: prComment.id, 24 | body: prComment.body.trim().toLowerCase(), 25 | created_at: prComment.created_at, 26 | repoId: repoId, 27 | pullRequestNo: context.issue.number 28 | }) 29 | }) 30 | listOfPRComments.map(comment => { 31 | if (isCommentSignedByUser(comment.body || "", comment.name)) { 32 | filteredListOfPRComments.push(comment) 33 | } 34 | }) 35 | for (var i = 0; i < filteredListOfPRComments.length; i++) { 36 | delete filteredListOfPRComments[i].body 37 | } 38 | /* 39 | *checking if the reacted committers are not the signed committers(not in the storage file) and filtering only the unsigned committers 40 | */ 41 | const newSigned = filteredListOfPRComments.filter(commentedCommitter => committerMap.notSigned!.some(notSignedCommitter => commentedCommitter.id === notSignedCommitter.id)) 42 | 43 | /* 44 | * checking if the commented users are only the contributors who has committed in the same PR (This is needed for the PR Comment and changing the status to success when all the contributors has reacted to the PR) 45 | */ 46 | const onlyCommitters = committers.filter(committer => filteredListOfPRComments.some(commentedCommitter => committer.id == commentedCommitter.id)) 47 | const commentedCommitterMap: ReactedCommitterMap = { 48 | newSigned, 49 | onlyCommitters, 50 | allSignedFlag: false 51 | } 52 | 53 | return commentedCommitterMap 54 | 55 | } 56 | 57 | function isCommentSignedByUser(comment: string, commentAuthor: string): boolean { 58 | if (commentAuthor === 'github-actions[bot]') { 59 | return false 60 | } 61 | if (getCustomPrSignComment() !== "") { 62 | return getCustomPrSignComment().toLowerCase() === comment 63 | } 64 | // using a `string` true or false purposely as github action input cannot have a boolean value 65 | switch (getUseDcoFlag()) { 66 | case 'true': 67 | return comment.match(/^.*i \s*have \s*read \s*the \s*dco \s*document \s*and \s*i \s*hereby \s*sign \s*the \s*dco.*$/) !== null 68 | case 'false': 69 | return comment.match(/^.*i \s*have \s*read \s*the \s*cla \s*document \s*and \s*i \s*hereby \s*sign \s*the \s*cla.*$/) !== null 70 | default: 71 | return false 72 | } 73 | } -------------------------------------------------------------------------------- /src/setupClaCheck.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import { context } from '@actions/github' 3 | import { checkAllowList } from './checkAllowList' 4 | import getCommitters from './graphql' 5 | import { 6 | ClafileContentAndSha, 7 | CommitterMap, 8 | CommittersDetails, 9 | ReactedCommitterMap 10 | } from './interfaces' 11 | import { 12 | createFile, 13 | getFileContent, 14 | updateFile 15 | } from './persistence/persistence' 16 | import prCommentSetup from './pullrequest/pullRequestComment' 17 | import { reRunLastWorkFlowIfRequired } from './pullRerunRunner' 18 | 19 | export async function setupClaCheck() { 20 | let committerMap = getInitialCommittersMap() 21 | 22 | let committers = await getCommitters() 23 | committers = checkAllowList(committers) 24 | 25 | const { claFileContent, sha } = (await getCLAFileContentandSHA( 26 | committers, 27 | committerMap 28 | )) as ClafileContentAndSha 29 | 30 | committerMap = prepareCommiterMap(committers, claFileContent) as CommitterMap 31 | 32 | try { 33 | const reactedCommitters = (await prCommentSetup( 34 | committerMap, 35 | committers 36 | )) as ReactedCommitterMap 37 | 38 | if (reactedCommitters?.newSigned.length) { 39 | /* pushing the recently signed contributors to the CLA Json File */ 40 | await updateFile(sha, claFileContent, reactedCommitters) 41 | } 42 | if ( 43 | reactedCommitters?.allSignedFlag || 44 | committerMap?.notSigned === undefined || 45 | committerMap.notSigned.length === 0 46 | ) { 47 | core.info(`All contributors have signed the CLA 📝 ✅ `) 48 | return reRunLastWorkFlowIfRequired() 49 | } else { 50 | core.setFailed( 51 | `Committers of Pull Request number ${context.issue.number} have to sign the CLA 📝` 52 | ) 53 | } 54 | } catch (err) { 55 | core.setFailed(`Could not update the JSON file: ${err.message}`) 56 | } 57 | } 58 | 59 | async function getCLAFileContentandSHA( 60 | committers: CommittersDetails[], 61 | committerMap: CommitterMap 62 | ): Promise { 63 | let result, claFileContentString, claFileContent, sha 64 | try { 65 | result = await getFileContent() 66 | } catch (error) { 67 | if (error.status === "404") { 68 | return createClaFileAndPRComment(committers, committerMap) 69 | } else { 70 | throw new Error( 71 | `Could not retrieve repository contents. Status: ${ 72 | error.status || 'unknown' 73 | }` 74 | ) 75 | } 76 | } 77 | sha = result?.data?.sha 78 | claFileContentString = Buffer.from(result.data.content, 'base64').toString() 79 | claFileContent = JSON.parse(claFileContentString) 80 | return { claFileContent, sha } 81 | } 82 | 83 | async function createClaFileAndPRComment( 84 | committers: CommittersDetails[], 85 | committerMap: CommitterMap 86 | ): Promise { 87 | committerMap.notSigned = committers 88 | committerMap.signed = [] 89 | committers.map(committer => { 90 | if (!committer.id) { 91 | committerMap.unknown.push(committer) 92 | } 93 | }) 94 | 95 | const initialContent = { signedContributors: [] } 96 | const initialContentString = JSON.stringify(initialContent, null, 3) 97 | const initialContentBinary = 98 | Buffer.from(initialContentString).toString('base64') 99 | 100 | await createFile(initialContentBinary).catch(error => 101 | core.setFailed( 102 | `Error occurred when creating the signed contributors file: ${ 103 | error.message || error 104 | }. Make sure the branch where signatures are stored is NOT protected.` 105 | ) 106 | ) 107 | await prCommentSetup(committerMap, committers) 108 | throw new Error( 109 | `Committers of pull request ${context.issue.number} have to sign the CLA` 110 | ) 111 | } 112 | 113 | function prepareCommiterMap( 114 | committers: CommittersDetails[], 115 | claFileContent 116 | ): CommitterMap { 117 | let committerMap = getInitialCommittersMap() 118 | 119 | committerMap.notSigned = committers.filter( 120 | committer => 121 | !claFileContent?.signedContributors.some(cla => committer.id === cla.id) 122 | ) 123 | committerMap.signed = committers.filter(committer => 124 | claFileContent?.signedContributors.some(cla => committer.id === cla.id) 125 | ) 126 | committers.map(committer => { 127 | if (!committer.id) { 128 | committerMap.unknown.push(committer) 129 | } 130 | }) 131 | return committerMap 132 | } 133 | 134 | const getInitialCommittersMap = (): CommitterMap => ({ 135 | signed: [], 136 | notSigned: [], 137 | unknown: [] 138 | }) 139 | -------------------------------------------------------------------------------- /src/shared/getInputs.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | 3 | export const getRemoteRepoName = (): string => { 4 | return core.getInput('remote-repository-name', { required: false }) 5 | } 6 | 7 | export const getRemoteOrgName = (): string => { 8 | return core.getInput('remote-organization-name', { required: false }) 9 | } 10 | 11 | export const getPathToSignatures = (): string => 12 | core.getInput('path-to-signatures', { required: false }) 13 | 14 | export const getPathToDocument = (): string => 15 | core.getInput('path-to-document', { required: false }) 16 | 17 | export const getBranch = (): string => 18 | core.getInput('branch', { required: false }) 19 | 20 | export const getAllowListItem = (): string => 21 | core.getInput('allowlist', { required: false }) 22 | 23 | export const getEmptyCommitFlag = (): string => 24 | core.getInput('empty-commit-flag', { required: false }) 25 | 26 | export const getSignedCommitMessage = (): string => 27 | core.getInput('signed-commit-message', { required: false }) 28 | 29 | export const getCreateFileCommitMessage = (): string => 30 | core.getInput('create-file-commit-message', { required: false }) 31 | 32 | export const getCustomNotSignedPrComment = (): string => 33 | core.getInput('custom-notsigned-prcomment', { required: false }) 34 | 35 | export const getCustomAllSignedPrComment = (): string => 36 | core.getInput('custom-allsigned-prcomment', { required: false }) 37 | 38 | export const getUseDcoFlag = (): string => 39 | core.getInput('use-dco-flag', { required: false }) 40 | 41 | export const getCustomPrSignComment = (): string => 42 | core.getInput('custom-pr-sign-comment', { required: false }) 43 | 44 | export const lockPullRequestAfterMerge = (): string => 45 | core.getInput('lock-pullrequest-aftermerge', { required: false }) 46 | 47 | export const suggestRecheck = (): string => 48 | core.getInput('suggest-recheck', { required: false }) 49 | -------------------------------------------------------------------------------- /src/shared/pr-sign-comment.ts: -------------------------------------------------------------------------------- 1 | import * as input from './getInputs' 2 | 3 | export function getPrSignComment() { 4 | return input.getCustomPrSignComment() || "I have read the CLA Document and I hereby sign the CLA" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "useUnknownInCatchVariables":false, 7 | "rootDir": "./src", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"] 13 | } 14 | --------------------------------------------------------------------------------