Propose kredits for PR reviews #71

Merged
raucao merged 5 commits from feature/review-kredits into master 2025-02-10 10:57:45 +00:00
6 changed files with 277 additions and 133 deletions

120
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@ethersproject/experimental": "5.7.0", "@ethersproject/experimental": "5.7.0",
"@kredits/contracts": "^7.3.0", "@kredits/contracts": "^7.3.0",
"axios": "^1.7.9",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eth-provider": "^0.13.6", "eth-provider": "^0.13.6",
@ -987,6 +988,21 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-code-frame": { "node_modules/babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -1351,6 +1367,17 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1482,6 +1509,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -1892,6 +1927,25 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-in": { "node_modules/for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -1911,6 +1965,19 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -3000,6 +3067,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@ -4279,6 +4351,21 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
}, },
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"babel-code-frame": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -4598,6 +4685,14 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -4702,6 +4797,11 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": { "depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -5017,6 +5117,11 @@
"unpipe": "~1.0.0" "unpipe": "~1.0.0"
} }
}, },
"follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -5030,6 +5135,16 @@
"for-in": "^1.0.1" "for-in": "^1.0.1"
} }
}, },
"form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": { "forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -5859,6 +5974,11 @@
"ipaddr.js": "1.9.1" "ipaddr.js": "1.9.1"
} }
}, },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"qs": { "qs": {
"version": "6.11.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@ethersproject/experimental": "5.7.0", "@ethersproject/experimental": "5.7.0",
"@kredits/contracts": "^7.3.0", "@kredits/contracts": "^7.3.0",
"axios": "^1.7.9",
slvrbckt marked this conversation as resolved
Review

Any particular reason for switching from the built in fetch to axios?

Any particular reason for switching from the built in fetch to axios?
Review

Just because that's what @galfert's code was using (at the time), and I didn't want to spend time on refactoring something that works. I'd rather move the whole thing away from hubot and npm modules eventually.

Just because that's what @galfert's code was using (at the time), and I didn't want to spend time on refactoring something that works. I'd rather move the whole thing away from hubot and npm modules eventually.
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eth-provider": "^0.13.6", "eth-provider": "^0.13.6",

View File

@ -1,29 +1,25 @@
const fetch = require('node-fetch'); const axios = require('axios');
module.exports = class GiteaReviews { module.exports = class GiteaReviews {
token = null; client = null;
kreditsAmounts = null; kreditsAmounts = null;
pageLimit = 100; pageLimit = 50;
constructor (token, kreditsAmounts) { constructor (token, kreditsAmounts) {
this.token = token;
this.kreditsAmounts = kreditsAmounts; this.kreditsAmounts = kreditsAmounts;
}
async request (path) { this.client = axios.create({
return fetch( baseURL: 'https://gitea.kosmos.org/api/v1',
`https://gitea.kosmos.org/api/v1${path}`, headers: {
{ 'accepts': 'application/json',
headers: { 'Authorization': `token ${token}`
'accepts': 'application/json',
'Authorization': `token ${this.token}`
}
} }
).then(response => response.json()); });
} }
async getReviewContributions (repos, startDate, endDate) { async getReviewContributions (repos, startDate, endDate) {
let pulls = [];
let reviewContributions = {} let reviewContributions = {}
await Promise.all(repos.map(async (repo) => { await Promise.all(repos.map(async (repo) => {
@ -32,17 +28,17 @@ module.exports = class GiteaReviews {
do { do {
try { try {
result = await this.request(`/repos/${repo}/pulls?state=closed&limit=${this.pageLimit}&page=${page}`); result = await this.client.get(`/repos/${repo}/pulls?state=closed&limit=${this.pageLimit}&page=${page}`);
} catch(error) { } catch(error) {
console.log(`failed to fetch PRs for repo ${repo}:`, error.message); console.log(`failed to fetch PRs for repo ${repo}:`, error.message);
continue; continue;
} }
if (!result || result.length === 0) { if (!result || !result.data || result.data.length === 0) {
continue; continue;
} }
let pullRequests = result.filter(pr => { let pullRequests = result.data.filter(pr => {
if (!pr.merged) return false; // only interested in merged PRs if (!pr.merged) return false; // only interested in merged PRs
// check if the PR has been merged in the given timeframe // check if the PR has been merged in the given timeframe
@ -56,21 +52,23 @@ module.exports = class GiteaReviews {
await Promise.all(pullRequests.map(async (pr) => { await Promise.all(pullRequests.map(async (pr) => {
let reviews; let reviews;
try { try {
reviews = await this.request(`/repos/${repo}/pulls/${pr.number}/reviews`); reviews = await this.client.get(`/repos/${repo}/pulls/${pr.number}/reviews`);
} catch(error) { } catch(error) {
console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message); console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message);
return; return;
} }
if (!reviews || reviews.length === 0) { if (!reviews || !reviews.data || reviews.data.length === 0) {
return; return;
} }
reviews = reviews.filter(review => { reviews = reviews.data.filter(review => {
return ['APPROVED', 'REJECTED'].includes(review.state); return ['APPROVED', 'REJECTED'].includes(review.state);
}); });
reviews.forEach(review => { reviews.forEach(review => {
// console.debug(`Review from /repos/${repo}/pulls/${pr.number}`);
if (typeof reviewContributions[review.user.login] === 'undefined') { if (typeof reviewContributions[review.user.login] === 'undefined') {
reviewContributions[review.user.login] = []; reviewContributions[review.user.login] = [];
} }
@ -88,7 +86,7 @@ module.exports = class GiteaReviews {
})); }));
page++; page++;
} while (result && result.length > 0); } while (result && result.data && result.data.length > 0);
})); }));
return reviewContributions; return reviewContributions;

View File

@ -1,30 +1,26 @@
const fetch = require('node-fetch'); const axios = require('axios');
module.exports = class GithubReviews { module.exports = class GithubReviews {
token = null; client = null;
kreditsAmounts = null; kreditsAmounts = null;
pageLimit = 100; pageLimit = 100;
constructor (token, kreditsAmounts) { constructor (token, kreditsAmounts) {
this.token = token;
this.kreditsAmounts = kreditsAmounts; this.kreditsAmounts = kreditsAmounts;
}
async request (path) { this.client = axios.create({
return fetch( baseURL: 'https://api.github.com',
`https://api.github.com${path}`, headers: {
{ 'Accept': 'application/vnd.github.v3+json',
headers: { 'User-Agent': 'Kosmos Kredits for reviews',
'Accept': 'application/vnd.github.v3+json', 'Authorization': `token ${token}`
'User-Agent': 'Kosmos Kredits for reviews',
'Authorization': `token ${this.token}`
}
} }
).then(response => response.json()); });
} }
async getReviewContributions (repos, startDate, endDate) { async getReviewContributions (repos, startDate, endDate) {
let pulls = [];
let reviewContributions = {} let reviewContributions = {}
await Promise.all(repos.map(async (repo) => { await Promise.all(repos.map(async (repo) => {
@ -33,17 +29,17 @@ module.exports = class GithubReviews {
do { do {
try { try {
result = await this.request(`/repos/${repo}/pulls?state=closed&perPage=${this.pageLimit}&page=${page}`); result = await this.client.get(`/repos/${repo}/pulls?state=closed&perPage=${this.pageLimit}&page=${page}`);
} catch(error) { } catch(error) {
console.log(`failed to fetch PRs for repo ${repo}:`, error.message); console.log(`failed to fetch PRs for repo ${repo}:`, error.message);
continue; continue;
} }
if (!result || result.length === 0) { if (!result || !result.data || result.data.length === 0) {
continue; continue;
} }
let pullRequests = result.filter(pr => { let pullRequests = result.data.filter(pr => {
if (!pr.merged_at) return false; // only interested in merged PRs if (!pr.merged_at) return false; // only interested in merged PRs
// check if the PR has been merged in the given timeframe // check if the PR has been merged in the given timeframe
@ -57,21 +53,23 @@ module.exports = class GithubReviews {
await Promise.all(pullRequests.map(async (pr) => { await Promise.all(pullRequests.map(async (pr) => {
let reviews; let reviews;
try { try {
reviews = await this.request(`/repos/${repo}/pulls/${pr.number}/reviews`); reviews = await this.client.get(`/repos/${repo}/pulls/${pr.number}/reviews`);
} catch(error) { } catch(error) {
console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message); console.log(`failed to fetch reviews for repo ${repo}, PR ${pr.number}:`, error.message);
return; return;
} }
if (!reviews || reviews.length === 0) { if (!reviews || !reviews.data || reviews.data.length === 0) {
return; return;
} }
reviews = reviews.filter(review => { reviews = reviews.data.filter(review => {
return ['APPROVED', 'REJECTED'].includes(review.state); return ['APPROVED', 'REJECTED'].includes(review.state);
}); });
reviews.forEach(review => { reviews.forEach(review => {
// console.debug(`Review from /repos/${repo}/pulls/${pr.number}`);
if (typeof reviewContributions[review.user.login] === 'undefined') { if (typeof reviewContributions[review.user.login] === 'undefined') {
reviewContributions[review.user.login] = []; reviewContributions[review.user.login] = [];
} }
@ -89,7 +87,7 @@ module.exports = class GithubReviews {
})); }));
page++; page++;
} while (result && result.length > 0); } while (result && result.data && result.data.length > 0);
})); }));
return reviewContributions; return reviewContributions;

View File

@ -1,25 +1,25 @@
{ {
"github": [ "github": [
"67P/botka",
"67P/hal8000",
"67P/hubot-kredits", "67P/hubot-kredits",
"67P/hubot-remotestorage-logger", "67P/hubot-remotestorage-logger",
"67P/hyperchannel", "67P/hyperchannel",
"67P/kosmos-schemas",
"67P/kredits-contracts",
"67P/kredits-signup", "67P/kredits-signup",
"67P/kredits-web", "67P/kredits-web",
"67P/remotestorage-module-kosmos",
"67P/waves", "67P/waves",
"sockethub/sockethub" "sockethub/sockethub"
], ],
"gitea": [ "gitea": [
"kosmos/akkounts", "kosmos/akkounts",
"kosmos/botka",
"kosmos/chef", "kosmos/chef",
"kosmos/gitea.kosmos.org", "kosmos/gitea.kosmos.org",
"kosmos/hal8000",
"kosmos/ipfs-cookbook", "kosmos/ipfs-cookbook",
"kosmos/kredits-ipfs-pinner", "kosmos/rs-module-kosmos",
"kosmos/schemas",
"kosmos/website", "kosmos/website",
"kosmos/wormhole" "kosmos/wormhole",
"kredits/contracts",
"kredits/ipfs-pinner"
] ]
} }

View File

@ -1,62 +1,68 @@
#!/usr/bin/env node #!/usr/bin/env node
require('dotenv').config({ path: '.env' }); require('dotenv').config();
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const util = require('util');
const fs = require('fs');
const GiteaReviews = require('./lib/gitea-reviews'); const GiteaReviews = require('./lib/gitea-reviews');
const GithubReviews = require('./lib/github-reviews'); const GithubReviews = require('./lib/github-reviews');
const ethers = require('ethers'); const ethers = require('ethers');
const NonceManager = require('@ethersproject/experimental').NonceManager; const NonceManager = require('@ethersproject/experimental').NonceManager;
const Kredits = require('kredits-contracts'); const Kredits = require('@kredits/contracts');
const util = require('util');
const yargs = require('yargs/yargs'); const walletPath = process.env.KREDITS_WALLET_PATH || './wallet.json';
const { hideBin } = require('yargs/helpers');
const fs = require('fs');
const walletPath = process.env.KREDITS_WALLET_PATH || '../wallet.json';
const walletJson = fs.readFileSync(walletPath); const walletJson = fs.readFileSync(walletPath);
const providerUrl = process.env.KREDITS_PROVIDER_URL; const providerUrl = process.env.KREDITS_PROVIDER_URL || 'http://localhost:7545';
const daoAddress = process.env.KREDITS_DAO_ADDRESS;
const ipfsConfig = { const ipfsConfig = {
host: process.env.IPFS_API_HOST || 'localhost', host: process.env.IPFS_API_HOST || 'localhost',
port: process.env.IPFS_API_PORT || '5001', port: process.env.IPFS_API_PORT || '5001',
protocol: process.env.IPFS_API_PROTOCOL || 'http' protocol: process.env.IPFS_API_PROTOCOL || 'http',
}; };
console.log('ipfsConfig:', ipfsConfig);
const kreditsAmounts = { const kreditsAmounts = {
'kredits-1': 100, 'kredits-1': 100,
'kredits-2': 300, 'kredits-2': 300,
'kredits-3': 1000 'kredits-3': 1000,
}; };
const repos = require('../repos.json'); const repos = require('./repos.json');
const argv = yargs(hideBin(process.argv)) const argv = yargs(hideBin(process.argv))
.option('start', { .option('start', {
alias: 's', alias: 's',
description: 'Include reviews for PRs merged after this date' description: 'Include reviews for PRs merged after this date',
}) })
.option('end', { .option('end', {
alias: 'e', alias: 'e',
description: 'Include reviews for PRs merged before this date' description: 'Include reviews for PRs merged before this date',
}) })
.option('dry', { .option('dry', {
alias: 'd', alias: 'd',
type: 'boolean', type: 'boolean',
description: 'Only list contribution details without creating them' description: 'Only list contribution details without creating them',
}) })
.help() .help()
.version() .version()
.demandOption('start', 'Please provide a start date') .demandOption('start', 'Please provide a start date')
.default('end', function now () { .default('end', function now() {
return (new Date()).toISOString().split('.')[0]+"Z"; return new Date().toISOString().split('.')[0] + 'Z';
}) })
.example([ .example([
['$0 --start 2020-11-01 --end 2020-11-30T23:59:59Z', 'Create contributions for reviews of pull requests merged in November 2020'], [
['$0 --start 2021-01-01', 'Create contributions for reviews of pull requests merged from Januar 2021 until now'], '$0 --start 2020-11-01 --end 2020-11-30T23:59:59Z',
]) 'Create contributions for reviews of pull requests merged in November 2020',
.argv ],
[
'$0 --start 2021-01-01',
'Create contributions for reviews of pull requests merged from Januar 2021 until now',
],
]).argv;
const startTimestamp = Date.parse(argv.start); const startTimestamp = Date.parse(argv.start);
const endTimestamp = Date.parse(argv.end); const endTimestamp = Date.parse(argv.end);
@ -71,50 +77,47 @@ if (isNaN(endTimestamp)) {
process.exit(1); process.exit(1);
} }
// check for existence of GITHUB_TOKEN and GITEA_TOKEN
if (!process.env.GITHUB_TOKEN || !process.env.GITEA_TOKEN) {
console.log('Please set both GITHUB_TOKEN and GITEA_TOKEN');
process.exit(1);
}
const startDate = new Date(startTimestamp); const startDate = new Date(startTimestamp);
const endDate = new Date(endTimestamp); const endDate = new Date(endTimestamp);
async function getAllReviews(repos, startDate, endDate) { async function getAllReviews(repos, startDate, endDate) {
const githubReviews = new GithubReviews(process.env.GITHUB_TOKEN, kreditsAmounts); const githubReviews = new GithubReviews(
const giteaReviews = new GiteaReviews(process.env.GITEA_TOKEN, kreditsAmounts); process.env.GITHUB_TOKEN,
kreditsAmounts
);
const giteaReviews = new GiteaReviews(
process.env.GITEA_TOKEN,
kreditsAmounts
);
return Promise.all([ return Promise.all([
githubReviews.getReviewContributions(repos.github, startDate, endDate), githubReviews.getReviewContributions(repos.github, startDate, endDate),
giteaReviews.getReviewContributions(repos.gitea, startDate, endDate) giteaReviews.getReviewContributions(repos.gitea, startDate, endDate),
]).then(reviews => { ]).then((reviews) => {
return { github: reviews[0], gitea: reviews[1] } return { github: reviews[0], gitea: reviews[1] };
}); });
} }
async function initializeKredits () { async function initializeKredits() {
// //
// Ethereum wallet setup // Wallet setup
// //
let wallet; let wallet;
try { try {
wallet = await ethers.Wallet.fromEncryptedJson(walletJson, process.env.KREDITS_WALLET_PASSWORD); wallet = await ethers.Wallet.fromEncryptedJson(walletJson, process.env.KREDITS_WALLET_PASSWORD);
} catch(error) { } catch(error) {
console.log('Could not load wallet:', error); console.warn('Could not load wallet:', error);
process.exit(1); process.exit(1);
} }
// //
// Ethereum provider/node setup // Solidity VM provider/node setup
// //
let ethProvider; console.log('Using blockchain node/API at', providerUrl);
if (providerUrl) {
ethProvider = new ethers.providers.JsonRpcProvider(providerUrl); const ethProvider = new ethers.providers.JsonRpcProvider(providerUrl);
} else {
ethProvider = new ethers.getDefaultProvider('rinkeby');
}
const signer = new NonceManager(wallet.connect(ethProvider)); const signer = new NonceManager(wallet.connect(ethProvider));
// //
@ -122,51 +125,58 @@ async function initializeKredits () {
// //
const opts = { ipfsConfig }; const opts = { ipfsConfig };
if (daoAddress) {
opts.addresses = { Kernel: daoAddress };
}
let kredits; let kredits;
try { try {
kredits = await new Kredits(signer.provider, signer, opts).init(); kredits = await new Kredits(signer.provider, signer, opts).init();
} catch(error) { } catch(error) {
console.log('Could not set up kredits:', error); console.warn('Could not set up kredits:', error);
process.exit(1); process.exit(1);
} }
console.log('Wallet address: ' + wallet.address);
//
// Check robot's wallet balance and alert when it's broke
//
ethProvider.getBalance(wallet.address).then(balance => {
console.log('Wallet balance: ' + ethers.utils.formatEther(balance) + ' RBTC');
if (balance.lt(ethers.utils.parseEther('0.0001'))) {
console.warn(`I\'m broke! Please send some RBTC to ${wallet.address} first.`);
process.exit(1);
}
});
return kredits; return kredits;
} }
function createContribution(contributorName, contributionAttributes, Contribution) { async function generateContributionData(reviews, Contributor) {
console.log(`Creating contribution token for ${contributionAttributes.amount}₭S to ${contributorName} for ${contributionAttributes.description}...`);
return Contribution.add(contributionAttributes).catch(error => {
console.log(`I tried to add a contribution for ${contributorName}, but I encountered an error when submitting the tx:`);
console.log(`Error:`, error);
console.log('Contribution attributes:');
console.log(util.inspect(contributionAttributes, { depth: 2, colors: true }));
});
}
async function generateContributionData(reviews, Contributor, startDate, endDate) {
const dateFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
const contributors = await Contributor.all(); const contributors = await Contributor.all();
const contributionData = {}; const contributionData = {};
const now = (new Date()).toISOString().split('.')[0]+"Z";
[date, time] = now.split('T'); const nextDay = new Date(endDate);
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
nextDay.setUTCHours(0, 0, 0, 0);
const nextDayStr = nextDay.toISOString().split('.')[0] + 'Z';
[date, time] = nextDayStr.split('T');
function addContributionDataForPlatform(platform) { function addContributionDataForPlatform(platform) {
for (const [username, platformReviews] of Object.entries(reviews[platform])) { for (const [username, platformReviews] of Object.entries(
const contributor = contributors.find(c => { reviews[platform]
)) {
const contributor = contributors.find((c) => {
return c[`${platform}_username`] === username; return c[`${platform}_username`] === username;
}); });
if (!contributor) { if (!contributor) {
console.log(`Could not find contributor for ${platform} user "${username}"`); console.log(
`Could not find contributor for ${platform} user "${username}"`
);
continue; continue;
} }
const urls = platformReviews.map(review => review.pr.html_url); const urls = platformReviews.map((review) => review.pr.html_url);
const kreditsAmount = platformReviews.reduce((amount, review) => { const kreditsAmount = platformReviews.reduce((amount, review) => {
return review.kredits + amount; return review.kredits + amount;
}, 0); }, 0);
@ -175,9 +185,6 @@ async function generateContributionData(reviews, Contributor, startDate, endDate
contributionData[contributor.name].amount += kreditsAmount; contributionData[contributor.name].amount += kreditsAmount;
contributionData[contributor.name].details.pullRequests.push(...urls); contributionData[contributor.name].details.pullRequests.push(...urls);
} else { } else {
const formattedStartDate = startDate.toLocaleString('en-us', dateFormatOptions);
const formattedEndDate = endDate.toLocaleString('en-us', dateFormatOptions);
contributionData[contributor.name] = { contributionData[contributor.name] = {
contributorId: contributor.id, contributorId: contributor.id,
contributorIpfsHash: contributor.ipfsHash, contributorIpfsHash: contributor.ipfsHash,
@ -185,11 +192,11 @@ async function generateContributionData(reviews, Contributor, startDate, endDate
time, time,
amount: kreditsAmount, amount: kreditsAmount,
kind: 'dev', kind: 'dev',
description: `PR reviews from ${formattedStartDate} to ${formattedEndDate}`,
details: { details: {
'pullRequests': urls kind: 'review',
} pullRequests: urls,
} },
};
} }
} }
} }
@ -200,20 +207,40 @@ async function generateContributionData(reviews, Contributor, startDate, endDate
return contributionData; return contributionData;
} }
Promise.all([initializeKredits(), getAllReviews(repos, startDate, endDate)]).then((values) => { Promise.all([
initializeKredits(),
getAllReviews(repos, startDate, endDate),
]).then((values) => {
const kredits = values[0]; const kredits = values[0];
const reviews = values[1]; const reviews = values[1];
const Contributor = kredits.Contributor;
const Contribution = kredits.Contribution;
generateContributionData(reviews, kredits.Contributor, startDate, endDate).then(contributionData => { async function createContribution(nickname, attrs) {
if (argv.dry) { console.log(`Creating review contribution for ${nickname}...`);
console.log('Contributions:'); console.log(util.inspect(attrs, { depth: 1, colors: true }));
console.log(util.inspect(contributionData, { depth: 3, colors: true }));
} else { return Contribution.add(attrs)
// create contributions .catch(error => {
for (const [username, contributionAttributes] of Object.entries(contributionData)) { console.error(`Error:`, error.message);
createContribution(username, contributionAttributes, kredits.Contribution); });
}
generateContributionData(reviews, Contributor).then(
async (contributionData) => {
if (argv.dry) {
console.log('Contributions:');
console.log(util.inspect(contributionData, { depth: 3, colors: true }));
return;
} else {
for (const nickname of Object.keys(contributionData)) {
const description = `Reviewed ${contributionData[nickname].details.pullRequests.length} pull requests (from ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]})`;
await createContribution(nickname, {
...contributionData[nickname],
description,
});
}
} }
} }
}); );
}); });