first commit from ibakshay fork

This commit is contained in:
d050420
2020-01-10 09:55:48 +01:00
commit f82c4e4748
31 changed files with 7384 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
__tests__/runner/*
.vscode
+165
View File
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
+81
View File
@@ -0,0 +1,81 @@
# WORK IN PROGRESS
## CLA GitHub Action
We are excited to announce that we are in development of CLA Assistant Github Action (a light version of CLA Assistant). With this Action, we can get rid of the need of a centrally managed database and rather storing all the contributors data in the repository's file system itself. This action won't be able to provide all the feature-sets of the current `CLA Assistant OAuth App`, However, this will work just fine If you want to configure CLA Assistant for a repo and store all the contributors data inside the repository. Feel free to test this GitHub Action and give us the feedback.
### Features
1. decentralized data storage
1. fully integrated with github environment
1. no UI required
1. no need for permission/scope handling
1. contributors can sign the CLA by just posting a Pull Request Comment
1. Signatures will be stored in a file inside the repository
## Configure Contributor License Agreement within two minutes
#### 1. Add the following Workflow File to your repository in this path`.github/workflow/cla.yml`
```yml
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request:
types: [opened,closed,synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheckcla' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request'
uses: cla-assistant/github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path-to-signatures: 'signatures/version1/cla.json'
path-To-cladocument: 'https://github.com/ibakshay/test-action-workflow/blob/master/cla.md'
branch: 'master'
```
#### 2. Pull Request event triggers CLA Workflow
CLA action Workflow will be triggered on all Pull Request `opened, synchronize`
<br/>
#### 3. Signing the CLA
CLA workflow creates a comment on Pull Request asking all the contributors who has not signed the CLA to sign and also fails the pull request status check with an `failure`. The contributors can sign the CLA within the pull request by just copy and pasting **"I have read the CLA Document and I hereby sign the CLA"** as a Pull Request comment like below.
If the contributors has already signed the CLA then the PR status will pass with `success`. <br/> By default, the GitHub Action bot will also do a empty commit with message **"@#contributorname# has signed the CLA"** whenever a contributor signs the CLA.
![Screenshot 2020-01-08 at 14 16 37](https://user-images.githubusercontent.com/33329946/71981019-c219c600-3221-11ea-874b-bb12107e77a9.png)
<br/>
#### 4. Signatures stored in a JSON file
After the contributor signed the CLA, the contributor's signature with metadata will be stored in a json file inside the repository like below screenshot and you can specify the custom path to this file with `path-to-signatures` input in the workflow . <br/> The default path is `path-to-signatures: 'signatures/version1/cla.json'`
![Screenshot 2020-01-07 at 16 13 43](https://user-images.githubusercontent.com/33329946/71905595-c33aec80-3168-11ea-8a08-c78f13cb0dcb.png)
### Environmental Variables :
| Name | Requirement | Description |
| --------------------- | ----------- | ----------- |
| `GITHUB_TOKEN` | _required_ | Must be in the form of `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/automating-your-workflow-with-github-actions/authenticating-with-the-github_token#about-the-github_token-secret)|
### Inputs Description :
| Name | Requirement | Description |
| --------------------- | ----------- | ----------- |
| `path-To-cladocument` | _required_ | provide full URL `https://<clafile>` to the Contributor License Agreement (CLA) to which the Contributor can read before signing the CLA. It can be a file inside the repository or it can be a gist |
| `path-to-signatures` | _optional_ | Path to the JSON file where all the signatures of the contributors will be stored inside the repository. Default path is "./signatures/cla.json". |
| `branch` | _optional_ | Branch in which all the signatures of the contributors will be stored and Default branch is `master` |
This action won't work for Pull Request coming from the forks as the [GitHub Action Token](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/authenticating-with-the-github_token) does not have write access for the forks, However, the GitHub team assured in one of the [discussion](https://github.community/t5/GitHub-Actions/Github-Workflow-not-running-from-pull-request-from-forked/m-p/32979#M1325) that they will ship this feature to enable read/write access for the PRs coming from the forks.
## License
> LGPL -3.0
+108
View File
@@ -0,0 +1,108 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { context } from '@actions/github'
import { getclas } from '../src/checkcla'
import { lockPullRequest } from '../src/pullRequestLock'
import { run } from '../src/main'
import { mocked } from 'ts-jest/utils'
jest.mock('@actions/core')
jest.mock('@actions/github')
jest.mock('../src/pullRequestLock')
jest.mock('../src/checkcla')
const mockedGetClas = mocked(getclas)
const mockedLockPullRequest = mocked(lockPullRequest)
describe('Pull request event', () => {
beforeEach(async () => {
// @ts-ignore
github.context = {
eventName: 'pull_request',
ref: 'refs/pull/232/merge',
workflow: 'CLA Assistant',
action: 'ibakshaygithub-action-1',
actor: 'ibakshay',
payload: {
action: 'closed',
number: '1',
pull_request: {
number: 1,
title: 'test',
user: {
login: 'ibakshay',
},
},
repository: {
name: 'auto-assign',
owner: {
login: 'ibakshay',
},
},
},
repo: {
owner: 'ibakshay',
repo: 'auto-assign',
},
issue: {
owner: 'kentaro-m',
repo: 'auto-assign',
number: 1,
},
sha: ''
}
}
)
test('the lockPullRequest method should be called if there is a pull request merge/closed', async () => {
await run()
expect(mockedLockPullRequest).toHaveBeenCalled()
})
test('the checkcla method should not called if there is a pull request merge/closed', async () => {
await run()
expect(mockedGetClas).not.toHaveBeenCalled()
})
test('the lockPullRequest method should not be called if there is a pull request opened', async () => {
github.context.payload.action = 'opened'
await run()
expect(mockedLockPullRequest).not.toHaveBeenCalled()
})
test('the checkcla method should be called if there is a pull request opened', async () => {
github.context.payload.action = 'opened'
await run()
expect(mockedGetClas).toHaveBeenCalled()
})
test('the lockPullRequest method should not be called if there is a pull request sync', async () => {
github.context.payload.action = 'synchronize'
await run()
expect(mockedLockPullRequest).not.toHaveBeenCalled()
})
test('the checkcla method should be called if there is a pull request sync', async () => {
github.context.payload.action = 'synchronize'
await run()
expect(mockedGetClas).toHaveBeenCalled()
})
})
+12
View File
@@ -0,0 +1,12 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import { context } from '@actions/github'
import { getclas } from '../src/checkcla'
import { lockPullRequest } from '../src/pullRequestLock'
import { run } from '../src/main'
import { mocked } from 'ts-jest/utils'
jest.mock('@actions/core')
jest.mock('@actions/github')
//const mockedLockPullRequest = mocked(lockPullRequest)
+16
View File
@@ -0,0 +1,16 @@
name: 'Node 12 Template Action'
description: 'Get started with Node actions'
author: 'GitHub'
inputs:
path-to-signatures:
description: 'Give a path for storing CLAs in a json file '
default: './signatures/cla.json'
branch:
description: 'select a branch where all the CLAs are stored'
default: 'master'
blockchain-storage-flag:
description: 'Optional functionality to store all the blockchain signatures in the ethirium blockchain as a smart contract'
default: false
runs:
using: 'node12'
main: 'lib/main.js'
+22
View File
@@ -0,0 +1,22 @@
# Contributors
### Checkin
- Do checkin source (src)
- Do checkin build output (lib)
- Do checkin runtime node_modules
- Do not checkin devDependency node_modules (husky can help see below)
### devDependencies
In order to handle correctly checking in node_modules without devDependencies, we run [Husky](https://github.com/typicode/husky) before each commit.
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:
```
npm install # installs all devDependencies including Husky
git add abc.ext # Add the files you've changed. This should include files in src, lib, and node_modules (see above)
git commit -m "Informative commit message" # Commit. This will run Husky
```
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`.
It will also make sure these changes are appropriately included in your commit (no further work is needed)
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
+70
View File
@@ -0,0 +1,70 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const octokit_1 = __importDefault(require("./octokit"));
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
function addEmptyCommit() {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Adding empty commit with the contributor name who has signed the CLA `);
if (github_1.context.payload.comment) {
//Do empty commit only when the contributor signs the CLA with the PR comment
if (github_1.context.payload.comment.body === 'I have read the CLA Document and I hereby sign the CLA') {
try {
const message = ` @${github_1.context.payload.comment.user.login} has signed the CLA `;
const pullRequestResponse = yield octokit_1.default.pulls.get({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
pull_number: github_1.context.payload.issue.number
});
const baseCommit = yield octokit_1.default.git.getCommit({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
commit_sha: pullRequestResponse.data.head.sha
});
const tree = yield octokit_1.default.git.getTree({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
tree_sha: baseCommit.data.tree.sha
});
const newCommit = yield octokit_1.default.git.createCommit({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
message: message,
tree: tree.data.sha,
parents: [pullRequestResponse.data.head.sha]
});
return octokit_1.default.git.updateRef({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
ref: `heads/${pullRequestResponse.data.head.ref}`,
sha: newCommit.data.sha
});
}
catch (e) {
core.error(`failed when adding empty commit with the contributor's signature name `);
}
}
}
return;
});
}
exports.addEmptyCommit = addEmptyCommit;
+160
View File
@@ -0,0 +1,160 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const graphql_1 = __importDefault(require("./graphql"));
const octokit_1 = __importDefault(require("./octokit"));
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
const pullRequestComment_1 = __importDefault(require("./pullRequestComment"));
const addEmptyCommit_1 = require("./addEmptyCommit");
function prepareCommiterMap(committers, clas) {
let committerMap = {};
committerMap.notSigned = committers.filter(committer => !clas.signedContributors.some(cla => committer.id === cla.id));
committerMap.signed = committers.filter(committer => clas.signedContributors.some(cla => committer.id === cla.id));
committers.map((committer) => { if (!committer.id) {
committerMap.unknown.push(committer);
} });
return committerMap;
}
function updateFile(pathToClaSignatures, sha, contentBinary, branch, pullRequestNo) {
return __awaiter(this, void 0, void 0, function* () {
yield octokit_1.default.repos.createOrUpdateFile({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
path: pathToClaSignatures,
sha: sha,
message: `**CLA ASSISTANT ACTION** Updating file for storing signatures from Pull Request ${pullRequestNo}`,
content: contentBinary,
branch: branch
});
//Do empty commit only when the contributor signs the CLA with the PR comment
yield addEmptyCommit_1.addEmptyCommit();
core.info(`successfully added empty commit with the contributor's signature name who has signed the CLA`);
});
}
function createFile(pathToClaSignatures, contentBinary, branch) {
return __awaiter(this, void 0, void 0, function* () {
/* TODO: add dynamic Message content */
let response = yield octokit_1.default.repos.createOrUpdateFile({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
path: pathToClaSignatures,
message: '**CLA ASSISTANT ACTION** Creating file for storing CLA Signatures',
content: contentBinary,
branch: branch
});
return response;
});
}
function getclas(pullRequestNo) {
return __awaiter(this, void 0, void 0, function* () {
let committerMap = {};
let signed = false;
//getting the path of the cla from the user
let pathToClaSignatures = core.getInput('path-to-signatures');
let branch = core.getInput('branch');
if (!pathToClaSignatures || pathToClaSignatures == '') {
pathToClaSignatures = 'signatures/cla.json'; // default path for storing the signatures
}
if (!branch || branch == '') {
branch = 'master';
}
let result, clas, sha;
const committers = yield graphql_1.default();
try {
result = yield octokit_1.default.repos.getContents({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
path: pathToClaSignatures,
ref: branch
});
sha = result.data.sha;
}
catch (error) {
if (error.status === 404) {
committerMap.notSigned = committers;
committerMap.signed = [];
committers.map((committer) => { if (!committer.id) {
committerMap.unknown.push(committer);
} });
const initialContent = { signedContributors: [] };
const initalContentString = JSON.stringify(initialContent, null, 2);
const initalContentBinary = Buffer.from(initalContentString).toString('base64');
const promise = Promise.all([createFile(pathToClaSignatures, initalContentBinary, branch), pullRequestComment_1.default(signed, committerMap, committers, pullRequestNo)]);
if (promise) {
core.setFailed(`committers of pull request ${github_1.context.issue.number} has to sign the CLA`);
return;
}
core.setFailed('error occured when creating the signed contributors file ' + error);
}
else {
core.setFailed(error.message);
}
}
clas = Buffer.from(result.data.content, 'base64').toString();
clas = JSON.parse(clas);
committerMap = prepareCommiterMap(committers, clas);
core.debug('unsigned contributors are: ' + JSON.stringify(committerMap.notSigned, null, 2));
core.debug('signed contributors are: ' + JSON.stringify(committerMap.signed, null, 2));
//DO NULL CHECK FOR below
if (committerMap) {
if (committerMap.notSigned.length === 0) {
core.debug("null check");
signed = true;
}
}
try {
const reactedCommitters = yield pullRequestComment_1.default(signed, committerMap, committers, pullRequestNo);
/* pushing the unsigned contributors to the CLA Json File */
if (signed) {
core.info("All committers have signed the CLA");
return;
}
if (reactedCommitters) {
if (reactedCommitters.newSigned) {
//reactedCommitters.newSigned.forEach((reactedCommitter) => reactedCommitter.pullRequestNo = pullRequestNo)
clas.signedContributors.push(...reactedCommitters.newSigned);
let contentString = JSON.stringify(clas, null, 2);
let contentBinary = Buffer.from(contentString).toString('base64');
yield updateFile(pathToClaSignatures, sha, contentBinary, branch, pullRequestNo);
}
if (reactedCommitters.allSignedFlag) {
core.info("All committers have signed the CLA");
return;
}
}
/* return when there are no unsigned committers */
if ((committerMap.notSigned === undefined || committerMap.notSigned.length === 0)) {
core.info("All committers have signed the CLA");
return;
}
else {
core.setFailed(`committers of Pull Request number ${github_1.context.issue.number} has to sign the CLA`);
}
}
catch (err) {
core.setFailed(err.message);
throw new Error("error while updating the JSON file" + err);
}
return clas;
});
}
exports.getclas = getclas;
+99
View File
@@ -0,0 +1,99 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const octokit_1 = __importDefault(require("./octokit"));
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
function getCommitters() {
return __awaiter(this, void 0, void 0, function* () {
try {
let committers = [];
let filteredCommitters = [];
let response = yield octokit_1.default.graphql(`
query($owner:String! $name:String! $number:Int! $cursor:String!){
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
commits(first: 100, after: $cursor) {
totalCount
edges {
node {
commit {
author {
email
name
user {
id
databaseId
login
}
}
committer {
name
user {
id
databaseId
login
}
}
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}`.replace(/ /g, ''), {
owner: github_1.context.repo.owner,
name: github_1.context.repo.repo,
number: github_1.context.issue.number,
cursor: ''
});
response.repository.pullRequest.commits.edges.forEach(edge => {
let committer = extractUserFromCommit(edge.node.commit);
let user = {
name: committer.login || committer.name,
id: committer.databaseId || '',
pullRequestNo: github_1.context.issue.number
};
if (committers.length === 0 || committers.map((c) => {
return c.name;
}).indexOf(user.name) < 0) {
committers.push(user);
}
});
console.log("the committers of this pr are" + JSON.stringify(committers));
filteredCommitters = committers.filter((committer) => {
return committer.id !== 41898282;
});
console.log("the committers last push of this pr are" + JSON.stringify(filteredCommitters));
return filteredCommitters;
}
catch (e) {
core.setFailed('graphql call to get the committers details failed:' + e);
}
});
}
exports.default = getCommitters;
const extractUserFromCommit = (commit) => commit.author.user || commit.committer.user || commit.author || commit.committer;
+2
View File
@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
+46
View File
@@ -0,0 +1,46 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
const io = require('@actions/io');
const checkcla_1 = require("./checkcla");
const pullRequestLock_1 = require("./pullRequestLock");
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const pullRequestNo = github_1.context.issue.number;
//s console.log("the context is " + JSON.stringify(context, null, 2))
core.info('CLA Assistant GitHub Action has started');
core.info('the PR No is ' + JSON.stringify(pullRequestNo));
if (github_1.context.payload.action === 'closed') {
return pullRequestLock_1.lockPullRequest(pullRequestNo);
}
else {
yield checkcla_1.getclas(pullRequestNo);
// const rateLimit = await octokit.rateLimit.get()
// console.log(JSON.stringify(rateLimit.data.resources, null, 4))
}
}
catch (error) {
core.setFailed(error.message);
}
});
}
exports.run = run;
run();
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const github_1 = require("@actions/github");
const octokit = new github_1.GitHub(process.env.GITHUB_TOKEN);
exports.default = octokit;
+176
View File
@@ -0,0 +1,176 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const octokit_1 = __importDefault(require("./octokit"));
const core = __importStar(require("@actions/core"));
const url_1 = require("./url");
const github_1 = require("@actions/github");
const signatureComment_1 = __importDefault(require("./signatureComment"));
function getComment() {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield octokit_1.default.issues.listComments({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: github_1.context.issue.number
});
return response.data.find(comment => comment.body.match(/.*CLA Assistant Lite.*/));
}
catch (e) {
core.setFailed('Error occured when getting all the comments of the pull request: ' + e.message);
}
});
}
function commentContent(signed, committerMap) {
const labelName = {};
if (signed) {
labelName.current_name = 'CLA signed :smiley:';
//updateLabel(signed, labelName)
return `**CLA Assistant Lite** All committers have signed the CLA. :smiley:`;
}
/* TODO: Unhandled Promise Rejection */
labelName.current_name = 'CLA Not Signed :worried:';
//updateLabel(signed, labelName)
let committersCount = 1;
if (committerMap && committerMap.signed && committerMap.notSigned) {
committersCount = committerMap.signed.length + committerMap.notSigned.length;
}
let you = (committersCount > 1 ? 'you all' : 'you');
let text = `**CLA Assistant Lite:** <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that ${you} sign our [Contributor License Agreement](${url_1.pathToCLADocument()}) before we can accept your contribution.You can sign the CLA by just posting a Pull Request Comment same as the below format.
***I have read the CLA Document and I hereby sign the CLA*** <br/><br/>`;
if (committersCount > 1 && committerMap && committerMap.signed && committerMap.notSigned) {
text += `**${committerMap.signed.length}** out of **${committerMap.signed.length + committerMap.notSigned.length}** committers have signed the CLA.`;
committerMap.signed.forEach((signedCommitter) => {
text += `<br/>:white_check_mark: @${signedCommitter.name}`;
});
committerMap.notSigned.forEach((unsignedCommitter) => {
text += `<br/>:x: @${unsignedCommitter.name}`;
});
text += '<br/>';
}
if (committerMap && committerMap.unknown && committerMap.unknown.length > 0) {
let seem = (committerMap.unknown.length > 1 ? 'seem' : 'seems');
text += `<hr/>**${committerMap.unknown.join(', ')}** ${seem} not to be a GitHub user.`;
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).<br/>';
}
return text;
}
function prepareAllSignedCommitters(committerMap, signedInPrCommitters, committers) {
let allSignedCommitters = [];
/*
Reference: https://stackoverflow.com/questions/54134156/javascript-merge-two-arrays-of-objects-only-if-not-duplicate-based-on-specifi
merging two arrays if not duplicate. 1) already signed committers in the file 2) signed committers in the PR comment
*/
let ids = new Set(signedInPrCommitters.map(committer => committer.id));
allSignedCommitters = [...signedInPrCommitters, ...committerMap.signed.filter(signedCommitter => !ids.has(signedCommitter.id))];
core.debug("all signed committers after merging " + JSON.stringify(allSignedCommitters, null, 2));
//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")
let allSignedFlag = committers.every(committer => allSignedCommitters.some(reactedCommitter => committer.id === reactedCommitter.id));
return allSignedFlag;
}
function prComment(signed, committerMap, committers, pullRequestNo) {
return __awaiter(this, void 0, void 0, function* () {
try {
const prComment = yield getComment();
if (!prComment) {
yield octokit_1.default.issues.createComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: github_1.context.issue.number,
body: commentContent(signed, committerMap)
});
return;
}
else if (prComment && prComment.id) {
if (signed) {
yield octokit_1.default.issues.updateComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
comment_id: prComment.id,
body: commentContent(signed, committerMap)
});
}
const reactedCommitters = yield signatureComment_1.default(prComment.id, committerMap, committers, pullRequestNo);
if (reactedCommitters) {
if (reactedCommitters.onlyCommitters) {
reactedCommitters.allSignedFlag = prepareAllSignedCommitters(committerMap, reactedCommitters.onlyCommitters, committers);
}
}
core.debug("allSignedFlag is " + reactedCommitters.allSignedFlag);
committerMap.signed.push(...reactedCommitters.newSigned);
committerMap.notSigned = committerMap.notSigned.filter(committer => !reactedCommitters.newSigned.some(reactedCommitter => committer.id === reactedCommitter.id));
yield octokit_1.default.issues.updateComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
comment_id: prComment.id,
body: commentContent(reactedCommitters.allSignedFlag, committerMap)
});
return reactedCommitters;
}
}
catch (e) {
core.setFailed('Error occured when creating or editing the comments of the pull request: ' + e.message);
}
});
}
exports.default = prComment;
// function addLabel() {
// return octokit.issues.addLabels({
// owner: context.repo.owner,
// repo: context.repo.repo,
// issue_number: context.issue.number,
// labels: ['CLA Not Signed :worried:']
// })
// }
// async function updateLabel(signed: boolean, labelName: LabelName) {
// try {
// const getLabel = await octokit.issues.getLabel({
// owner: context.repo.owner,
// repo: context.repo.repo,
// name: labelName.current_name
// })
// if (getLabel) {
// return
// }
// } catch (error) {
// if (error.status === 404) {
// if (signed) {
// labelName = {
// current_name: 'CLA Not Signed :worried:',
// name: 'CLA signed :smiley:'
// }
// }
// else {
// labelName = {
// current_name: 'CLA signed :smiley:',
// name: 'CLA Not Signed :worried:'
// }
// }
// return octokit.issues.updateLabel({
// owner: context.repo.owner,
// repo: context.repo.repo,
// current_name: labelName.current_name,
// name: labelName.name
// })
// }
// core.setFailed("error when creating a label :" + error)
// }
// }
+41
View File
@@ -0,0 +1,41 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const octokit_1 = __importDefault(require("./octokit"));
const core = __importStar(require("@actions/core"));
const github_1 = require("@actions/github");
function lockPullRequest(pullRequestNo) {
return __awaiter(this, void 0, void 0, function* () {
core.info('Locking the Pull Request to safe guard the Pull Request CLA Signatures');
try {
yield octokit_1.default.issues.lock({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: pullRequestNo
});
core.info(`successfully locked the pull request ${pullRequestNo}`);
}
catch (e) {
core.error(`failed when locking the pull request `);
}
});
}
exports.lockPullRequest = lockPullRequest;
+98
View File
@@ -0,0 +1,98 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const octokit_1 = __importDefault(require("./octokit"));
const github_1 = require("@actions/github");
const fetch = require("node-fetch");
const core = __importStar(require("@actions/core"));
function webhookSmartContract(newSignedCommitters) {
return __awaiter(this, void 0, void 0, function* () {
const blockchainURL = core.getInput('blockchain-webhook-endpoint') || 'https://u9afh6n36g.execute-api.eu-central-1.amazonaws.com/dev/webhook';
try {
const config = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(newSignedCommitters)
};
const res = yield fetch(blockchainURL, config);
const response = yield res.json();
core.debug("the response of the webhook is " + JSON.stringify(response));
//const response = await res.json()
if (response.success) {
core.debug("the response2 of the webhook is " + JSON.stringify(response));
//return json
return response;
}
}
catch (error) {
core.setFailed('The webhook post request for storing signatures in smart contract failed' + error);
}
});
}
function signatureWithPRComment(commentId, committerMap, committers, pullRequestNo) {
return __awaiter(this, void 0, void 0, function* () {
let blockchainFlag = core.getInput('blockchain-storage-flag');
core.debug("the blockchainflag is " + blockchainFlag);
let repoId = github_1.context.payload.repository.id;
let commentedCommitterMap = {};
let prResponse = yield octokit_1.default.issues.listComments({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: github_1.context.issue.number
});
let listOfPRComments = [];
let filteredListOfPRComments = [];
//TODO: Do null check for repoID
prResponse.data.map((prComment) => {
listOfPRComments.push({
name: prComment.user.login,
id: prComment.user.id,
comment_id: prComment.id,
body: prComment.body.toLowerCase(),
created_at: prComment.created_at,
repoId: repoId,
pullRequestNo: pullRequestNo
});
});
listOfPRComments.map((comment) => {
if (comment.body.match(/^.*i \s*have \s*read \s*the \s*cla \s*document \s*and \s*i \s*hereby \s*sign \s*the \s*cla.*$/) && comment.name !== 'github-actions[bot]') {
filteredListOfPRComments.push(comment);
}
});
for (var i = 0; i < filteredListOfPRComments.length; i++) {
delete filteredListOfPRComments[i].body;
}
// //checking if the reacted committers are not the signed committers(not in the storage file) and filtering only the unsigned committers
commentedCommitterMap.newSigned = filteredListOfPRComments.filter(commentedCommitter => committerMap.notSigned.some(notSignedCommitter => commentedCommitter.id === notSignedCommitter.id));
core.debug("the new commented committers(signed) are :" + JSON.stringify(commentedCommitterMap.newSigned, null, 3));
if (blockchainFlag == 'true' && commentedCommitterMap.newSigned) {
yield webhookSmartContract(commentedCommitterMap.newSigned);
}
//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)
commentedCommitterMap.onlyCommitters = committers.filter(committer => filteredListOfPRComments.some(commentedCommitter => committer.id == commentedCommitter.id));
core.debug("the reacted signed committers comments are " + JSON.stringify(commentedCommitterMap.onlyCommitters, null, 3));
return commentedCommitterMap;
});
}
exports.default = signatureWithPRComment;
+15
View File
@@ -0,0 +1,15 @@
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
function pathToCLADocument() {
const pathToCLADocument = core.getInput('path-To-cladocument');
return pathToCLADocument;
}
exports.pathToCLADocument = pathToCLADocument;
+5493
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "github-action",
"version": "0.0.1",
"description": "GitHub Action for storing CLA signatures",
"main": "lib/main.js",
"scripts": {
"test": "jest",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cla-assistant/github-action.git"
},
"keywords": [
"actions",
"node",
"setup"
],
"dependencies": {
"@actions/core": "^1.0.0",
"@actions/github": "^1.0.0",
"@actions/io": "^1.0.0",
"@types/jquery": "^3.3.31",
"actions-toolkit": "^2.1.0",
"lodash": "^4.17.15",
"node-fetch": "^2.6.0"
},
"devDependencies": {
"@types/jest": "^24.0.13",
"@types/node": "^12.0.4",
"jest": "^24.8.0",
"jest-circus": "^24.7.1",
"ts-jest": "^24.2.0",
"typescript": "^3.5.1"
},
"author": "ibakshay",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/cla-assistant/github-action/issues"
},
"homepage": "https://github.com/cla-assistant/github-action#readme"
}
+53
View File
@@ -0,0 +1,53 @@
import octokit from './octokit'
import * as core from '@actions/core'
import { context } from '@actions/github'
export async function addEmptyCommit() {
core.info(`Adding empty commit with the contributor name who has signed the CLA `)
if (context.payload.comment) {
//Do empty commit only when the contributor signs the CLA with the PR comment
if (context.payload.comment.body === 'I have read the CLA Document and I hereby sign the CLA') {
try {
const message = ` @${context.payload.comment.user.login} has signed the CLA `
const pullRequestResponse = await octokit.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue!.number
})
const baseCommit = await octokit.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: pullRequestResponse.data.head.sha
})
const tree = await octokit.git.getTree({
owner: context.repo.owner,
repo: context.repo.repo,
tree_sha: baseCommit.data.tree.sha
})
const newCommit = await octokit.git.createCommit(
{
owner: context.repo.owner,
repo: context.repo.repo,
message: message,
tree: tree.data.sha,
parents: [pullRequestResponse.data.head.sha]
}
)
return octokit.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${pullRequestResponse.data.head.ref}`,
sha: newCommit.data.sha
})
} catch (e) {
core.error(`failed when adding empty commit with the contributor's signature name `)
}
}
}
return
}
+150
View File
@@ -0,0 +1,150 @@
import getCommitters from './graphql'
import octokit from './octokit'
import * as core from '@actions/core'
import { context } from '@actions/github'
import prComment from './pullRequestComment'
import { addEmptyCommit } from './addEmptyCommit'
import { CommitterMap, CommittersDetails, ReactedCommitterMap } from './interfaces'
function prepareCommiterMap(committers: CommittersDetails[], clas): CommitterMap {
let committerMap: CommitterMap = {}
committerMap.notSigned = committers.filter(committer => !clas.signedContributors.some(cla => committer.id === cla.id))
committerMap.signed = committers.filter(committer => clas.signedContributors.some(cla => committer.id === cla.id))
committers.map((committer) => { if (!committer.id) { committerMap.unknown!.push(committer) } })
return committerMap
}
async function updateFile(pathToClaSignatures, sha, contentBinary, branch, pullRequestNo) {
await octokit.repos.createOrUpdateFile({
owner: context.repo.owner,
repo: context.repo.repo,
path: pathToClaSignatures,
sha: sha,
message: `**CLA ASSISTANT ACTION** Updating file for storing signatures from Pull Request ${pullRequestNo}`,
content: contentBinary,
branch: branch
})
//Do empty commit only when the contributor signs the CLA with the PR comment
await addEmptyCommit()
core.info(`successfully added empty commit with the contributor's signature name who has signed the CLA`)
}
async function createFile(pathToClaSignatures, contentBinary, branch): Promise<object> {
/* TODO: add dynamic Message content */
let response = await octokit.repos.createOrUpdateFile({
owner: context.repo.owner,
repo: context.repo.repo,
path: pathToClaSignatures,
message: '**CLA ASSISTANT ACTION** Creating file for storing CLA Signatures',
content: contentBinary,
branch: branch
})
return response
}
export async function getclas(pullRequestNo: number) {
let committerMap = {} as CommitterMap
let signed: boolean = false
//getting the path of the cla from the user
let pathToClaSignatures: string = core.getInput('path-to-signatures')
let branch: string = core.getInput('branch')
if (!pathToClaSignatures || pathToClaSignatures == '') {
pathToClaSignatures = 'signatures/cla.json' // default path for storing the signatures
}
if (!branch || branch == '') {
branch = 'master'
}
let result, clas, sha
const committers = await getCommitters() as CommittersDetails[]
try {
result = await octokit.repos.getContents({
owner: context.repo.owner,
repo: context.repo.repo,
path: pathToClaSignatures,
ref: branch
})
sha = result.data.sha
} catch (error) {
if (error.status === 404) {
committerMap.notSigned = committers
committerMap.signed = []
committers.map((committer) => { if (!committer.id) { committerMap.unknown!.push(committer) } })
const initialContent = { signedContributors: [] }
const initalContentString = JSON.stringify(initialContent, null, 2)
const initalContentBinary = Buffer.from(initalContentString).toString('base64')
const promise = Promise.all([createFile(pathToClaSignatures, initalContentBinary, branch), prComment(signed, committerMap, committers, pullRequestNo)])
if (promise) {
core.setFailed(`committers of pull request ${context.issue.number} has to sign the CLA`)
return
}
core.setFailed('error occured when creating the signed contributors file ' + error)
}
else {
core.setFailed(error.message)
}
}
clas = Buffer.from(result.data.content, 'base64').toString()
clas = JSON.parse(clas)
committerMap = prepareCommiterMap(committers, clas) as CommitterMap
core.debug('unsigned contributors are: ' + JSON.stringify(committerMap.notSigned, null, 2))
core.debug('signed contributors are: ' + JSON.stringify(committerMap.signed, null, 2))
//DO NULL CHECK FOR below
if (committerMap) {
if (committerMap.notSigned!.length === 0) {
core.debug("null check")
signed = true
}
}
try {
const reactedCommitters: ReactedCommitterMap = await prComment(signed, committerMap, committers, pullRequestNo) as ReactedCommitterMap
/* pushing the unsigned contributors to the CLA Json File */
if (signed) {
core.info("All committers have signed the CLA")
return
}
if (reactedCommitters) {
if (reactedCommitters.newSigned) {
//reactedCommitters.newSigned.forEach((reactedCommitter) => reactedCommitter.pullRequestNo = pullRequestNo)
clas.signedContributors.push(...reactedCommitters.newSigned)
let contentString = JSON.stringify(clas, null, 2)
let contentBinary = Buffer.from(contentString).toString('base64')
await updateFile(pathToClaSignatures, sha, contentBinary, branch, pullRequestNo)
}
if (reactedCommitters.allSignedFlag) {
core.info("All committers have signed the CLA")
return
}
}
/* return when there are no unsigned committers */
if ((committerMap.notSigned === undefined || committerMap.notSigned.length === 0)) {
core.info("All committers have signed the CLA")
return
}
else {
core.setFailed(`committers of Pull Request number ${context.issue.number} has to sign the CLA`)
}
}
catch (err) {
core.setFailed(err.message)
throw new Error("error while updating the JSON file" + err)
}
return clas
}
+80
View File
@@ -0,0 +1,80 @@
import octokit from './octokit'
import * as core from '@actions/core';
import { context } from '@actions/github'
import { CommittersDetails } from './interfaces'
export default async function getCommitters() {
try {
let committers: CommittersDetails[] = []
let filteredCommitters: CommittersDetails[] = []
let response = await octokit.graphql(`
query($owner:String! $name:String! $number:Int! $cursor:String!){
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
commits(first: 100, after: $cursor) {
totalCount
edges {
node {
commit {
author {
email
name
user {
id
databaseId
login
}
}
committer {
name
user {
id
databaseId
login
}
}
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
}`.replace(/ /g, ''), {
owner: context.repo.owner,
name: context.repo.repo,
number: context.issue.number,
cursor: ''
})
response.repository.pullRequest.commits.edges.forEach(edge => {
let committer = extractUserFromCommit(edge.node.commit)
let user = {
name: committer.login || committer.name,
id: committer.databaseId || '',
pullRequestNo: context.issue.number
}
if (committers.length === 0 || committers.map((c) => {
return c.name
}).indexOf(user.name) < 0) {
committers.push(user)
}
})
console.log("the committers of this pr are" + JSON.stringify(committers))
filteredCommitters = committers.filter((committer) => {
return committer.id !== 41898282
})
console.log("the committers last push of this pr are" + JSON.stringify(filteredCommitters))
return filteredCommitters
} catch (e) {
core.setFailed('graphql call to get the committers details failed:' + e)
}
}
const extractUserFromCommit = (commit) => commit.author.user || commit.committer.user || commit.author || commit.committer
+42
View File
@@ -0,0 +1,42 @@
export interface CommitterMap {
signed?: CommittersDetails[],
notSigned?: CommittersDetails[],
unknown?: CommittersDetails[]
}
export interface ReactedCommitterMap {
newSigned: CommittersDetails[],
onlyCommitters?: CommittersDetails[],
allSignedFlag: boolean
}
export interface CommentedCommitterMap {
newSigned: CommittersDetails[],
onlyCommitters?: CommittersDetails[],
allSignedFlag: boolean
}
export interface CommittersDetails {
name: string,
id: number,
pullRequestNo?: number,
created_at?: string,
updated_at?: string
comment_id?: number,
body?: string,
repoId?: string
}
export interface LabelName {
current_name: string,
name: string
}
export interface CommittersCommentDetails {
name: string,
id: number,
comment_id: number,
body: string,
created_at: string,
updated_at: string
}
+30
View File
@@ -0,0 +1,30 @@
import * as core from '@actions/core'
import { context } from '@actions/github'
const io = require('@actions/io')
import { getclas } from './checkcla'
import { lockPullRequest } from './pullRequestLock'
export async function run() {
try {
const pullRequestNo: number = context.issue.number
//s console.log("the context is " + JSON.stringify(context, null, 2))
core.info('CLA Assistant GitHub Action has started')
core.info('the PR No is ' + JSON.stringify(pullRequestNo))
if (context.payload.action === 'closed') {
return lockPullRequest(pullRequestNo)
}
else {
await getclas(pullRequestNo)
// const rateLimit = await octokit.rateLimit.get()
// console.log(JSON.stringify(rateLimit.data.resources, null, 4))
}
} catch (error) {
core.setFailed(error.message)
}
}
run()
+5
View File
@@ -0,0 +1,5 @@
import { GitHub } from '@actions/github'
const octokit = new GitHub(process.env.GITHUB_TOKEN as string)
export default octokit
+176
View File
@@ -0,0 +1,176 @@
import octokit from './octokit'
import * as core from '@actions/core'
import { pathToCLADocument } from './url'
import { context } from '@actions/github'
import signatureWithPRComment from './signatureComment'
import { CommitterMap, ReactedCommitterMap, LabelName, CommittersDetails } from './interfaces'
async function getComment() {
try {
const response = await octokit.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
})
return response.data.find(comment => comment.body.match(/.*CLA Assistant Lite.*/))
} catch (e) {
core.setFailed('Error occured when getting all the comments of the pull request: ' + e.message)
}
}
function commentContent(signed: boolean, committerMap: CommitterMap): string {
const labelName = {} as LabelName
if (signed) {
labelName.current_name = 'CLA signed :smiley:'
//updateLabel(signed, labelName)
return `**CLA Assistant Lite** All committers have signed the CLA. :smiley:`
}
/* TODO: Unhandled Promise Rejection */
labelName.current_name = 'CLA Not Signed :worried:'
//updateLabel(signed, labelName)
let committersCount = 1
if (committerMap && committerMap.signed && committerMap.notSigned) {
committersCount = committerMap.signed.length + committerMap.notSigned.length
}
let you = (committersCount > 1 ? 'you all' : 'you')
let text = `**CLA Assistant Lite:** <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that ${you} sign our [Contributor License Agreement](${pathToCLADocument()}) before we can accept your contribution.You can sign the CLA by just posting a Pull Request Comment same as the below format.
***I have read the CLA Document and I hereby sign the CLA*** <br/><br/>`
if (committersCount > 1 && committerMap && committerMap.signed && committerMap.notSigned) {
text += `**${committerMap.signed.length}** out of **${committerMap.signed.length + committerMap.notSigned.length}** committers have signed the CLA.`
committerMap.signed.forEach((signedCommitter) => {
text += `<br/>:white_check_mark: @${signedCommitter.name}`
})
committerMap.notSigned.forEach((unsignedCommitter) => {
text += `<br/>:x: @${unsignedCommitter.name}`
})
text += '<br/>'
}
if (committerMap && committerMap.unknown && committerMap.unknown.length > 0) {
let seem = (committerMap.unknown.length > 1 ? 'seem' : 'seems')
text += `<hr/>**${committerMap.unknown.join(', ')}** ${seem} not to be a GitHub user.`
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).<br/>'
}
return text
}
function prepareAllSignedCommitters(committerMap: CommitterMap, signedInPrCommitters: CommittersDetails[], committers: CommittersDetails[]): boolean {
let allSignedCommitters = [] as CommittersDetails[]
/*
Reference: https://stackoverflow.com/questions/54134156/javascript-merge-two-arrays-of-objects-only-if-not-duplicate-based-on-specifi
merging two arrays if not duplicate. 1) already signed committers in the file 2) signed committers in the PR comment
*/
let ids = new Set(signedInPrCommitters.map(committer => committer.id))
allSignedCommitters = [...signedInPrCommitters, ...committerMap.signed!.filter(signedCommitter => !ids.has(signedCommitter.id))]
core.debug("all signed committers after merging " + JSON.stringify(allSignedCommitters, null, 2))
//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")
let allSignedFlag: boolean = committers.every(committer => allSignedCommitters.some(reactedCommitter => committer.id === reactedCommitter.id))
return allSignedFlag
}
export default async function prComment(signed: boolean, committerMap: CommitterMap, committers: CommittersDetails[], pullRequestNo: number) {
try {
const prComment = await getComment()
if (!prComment) {
await octokit.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentContent(signed, committerMap)
})
return
}
else if (prComment && prComment.id) {
if (signed) {
await octokit.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: prComment.id,
body: commentContent(signed, committerMap)
})
}
const reactedCommitters: ReactedCommitterMap = await signatureWithPRComment(prComment.id, committerMap, committers, pullRequestNo) as ReactedCommitterMap
if (reactedCommitters) {
if (reactedCommitters.onlyCommitters) {
reactedCommitters.allSignedFlag = prepareAllSignedCommitters(committerMap, reactedCommitters.onlyCommitters, committers)
}
}
core.debug("allSignedFlag is " + reactedCommitters.allSignedFlag)
committerMap.signed!.push(...reactedCommitters.newSigned)
committerMap.notSigned = committerMap.notSigned!.filter(committer => !reactedCommitters.newSigned.some(reactedCommitter => committer.id === reactedCommitter.id))
await octokit.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: prComment.id,
body: commentContent(reactedCommitters.allSignedFlag, committerMap)
})
return reactedCommitters
}
} catch (e) {
core.setFailed('Error occured when creating or editing the comments of the pull request: ' + e.message)
}
}
// function addLabel() {
// return octokit.issues.addLabels({
// owner: context.repo.owner,
// repo: context.repo.repo,
// issue_number: context.issue.number,
// labels: ['CLA Not Signed :worried:']
// })
// }
// async function updateLabel(signed: boolean, labelName: LabelName) {
// try {
// const getLabel = await octokit.issues.getLabel({
// owner: context.repo.owner,
// repo: context.repo.repo,
// name: labelName.current_name
// })
// if (getLabel) {
// return
// }
// } catch (error) {
// if (error.status === 404) {
// if (signed) {
// labelName = {
// current_name: 'CLA Not Signed :worried:',
// name: 'CLA signed :smiley:'
// }
// }
// else {
// labelName = {
// current_name: 'CLA signed :smiley:',
// name: 'CLA Not Signed :worried:'
// }
// }
// return octokit.issues.updateLabel({
// owner: context.repo.owner,
// repo: context.repo.repo,
// current_name: labelName.current_name,
// name: labelName.name
// })
// }
// core.setFailed("error when creating a label :" + error)
// }
// }
+21
View File
@@ -0,0 +1,21 @@
import octokit from './octokit'
import * as core from '@actions/core'
import { context } from '@actions/github'
export async function lockPullRequest(pullRequestNo: number) {
core.info('Locking the Pull Request to safe guard the Pull Request CLA Signatures')
try {
await octokit.issues.lock(
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequestNo
}
)
core.info(`successfully locked the pull request ${pullRequestNo}`)
} catch (e) {
core.error(`failed when locking the pull request `)
}
}
+86
View File
@@ -0,0 +1,86 @@
import octokit from './octokit'
import { context } from '@actions/github'
import { CommitterMap, CommittersDetails, CommentedCommitterMap } from './interfaces'
const fetch = require("node-fetch");
import * as core from '@actions/core'
async function webhookSmartContract(newSignedCommitters: CommittersDetails[]) {
const blockchainURL = core.getInput('blockchain-webhook-endpoint') || 'https://u9afh6n36g.execute-api.eu-central-1.amazonaws.com/dev/webhook'
try {
const config = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(newSignedCommitters)
}
const res = await fetch(blockchainURL, config)
const response = await res.json()
core.debug("the response of the webhook is " + JSON.stringify(response))
//const response = await res.json()
if (response.success) {
core.debug("the response2 of the webhook is " + JSON.stringify(response))
//return json
return response
}
} catch (error) {
core.setFailed('The webhook post request for storing signatures in smart contract failed' + error)
}
}
export default async function signatureWithPRComment(commentId, committerMap: CommitterMap, committers, pullRequestNo: number) {
let blockchainFlag = core.getInput('blockchain-storage-flag')
core.debug("the blockchainflag is " + blockchainFlag)
let repoId = context.payload.repository!.id
let commentedCommitterMap = {} as CommentedCommitterMap
let prResponse = await octokit.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
})
let listOfPRComments = [] as CommittersDetails[]
let filteredListOfPRComments = [] as CommittersDetails[]
//TODO: Do null check for repoID
prResponse.data.map((prComment) => {
listOfPRComments.push({
name: prComment.user.login,
id: prComment.user.id,
comment_id: prComment.id,
body: prComment.body.toLowerCase(),
created_at: prComment.created_at,
repoId: repoId,
pullRequestNo: pullRequestNo
})
})
listOfPRComments.map((comment) => {
if (comment.body!.match(/^.*i \s*have \s*read \s*the \s*cla \s*document \s*and \s*i \s*hereby \s*sign \s*the \s*cla.*$/) && comment.name !== 'github-actions[bot]') {
filteredListOfPRComments.push(comment)
}
})
for (var i = 0; i < filteredListOfPRComments.length; i++) {
delete filteredListOfPRComments[i].body
}
// //checking if the reacted committers are not the signed committers(not in the storage file) and filtering only the unsigned committers
commentedCommitterMap.newSigned = filteredListOfPRComments.filter(commentedCommitter => committerMap.notSigned!.some(notSignedCommitter => commentedCommitter.id === notSignedCommitter.id))
core.debug("the new commented committers(signed) are :" + JSON.stringify(commentedCommitterMap.newSigned, null, 3))
if (blockchainFlag == 'true' && commentedCommitterMap.newSigned) {
await webhookSmartContract(commentedCommitterMap.newSigned)
}
//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)
commentedCommitterMap.onlyCommitters = committers.filter(committer => filteredListOfPRComments.some(commentedCommitter => committer.id == commentedCommitter.id))
core.debug("the reacted signed committers comments are " + JSON.stringify(commentedCommitterMap.onlyCommitters, null, 3))
return commentedCommitterMap
}
+7
View File
@@ -0,0 +1,7 @@
import * as core from '@actions/core'
export function pathToCLADocument() {
const pathToCLADocument = core.getInput('path-To-cladocument')
return pathToCLADocument
}
+69
View File
@@ -0,0 +1,69 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib",
/* Redirect output structure to the directory. */
"rootDir": "./src",
/* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true,
/* Enable all strict type-checking options. */
"noImplicitAny": false,
/* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"exclude": ["node_modules", "**/*.test.ts"]
}