mirror of
https://github.com/BradNut/awesome-uses
synced 2025-09-08 17:40:31 +00:00
feat: comment on PR with validation outcome (#503)
* comment on PR after validation run. * bork my url for sample output * populate GITHUB_TOKEN in action * more verbose logging * more logging * fix wrong import * logging * remove logging & implementation attempt * log out comment creation query output * styling * prettify comments + increase logging * more prettifying of the text * Revert "bork my url for sample output" This reverts commit 0c6d17450e753cd407db69a8129bf0cbc5831b88. * add data change to trigger GH action * improve messages... * change my URL so it appears in the message * more formatting of comments * move + rename commentPullRequest -> utils.communicateValationOutcome
This commit is contained in:
parent
ce7b253da5
commit
e20e0f5991
6 changed files with 261 additions and 14 deletions
2
.github/workflows/data-validate.yml
vendored
2
.github/workflows/data-validate.yml
vendored
|
|
@ -27,3 +27,5 @@ jobs:
|
|||
|
||||
- name: Validate data.js
|
||||
run: node ./scripts/data-validate.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
197
package-lock.json
generated
197
package-lock.json
generated
|
|
@ -17,6 +17,15 @@
|
|||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@actions/github": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/github/-/github-2.0.1.tgz",
|
||||
"integrity": "sha512-C7dAsCkpPi1HxTzLldz+oY+9c5G+nnaK7xgk8KA83VVGlrGK7d603E3snUAFocWrqEu/uvdYD82ytggjcpYSQA==",
|
||||
"requires": {
|
||||
"@octokit/graphql": "^4.3.1",
|
||||
"@octokit/rest": "^16.15.0"
|
||||
}
|
||||
},
|
||||
"@actions/io": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
|
||||
|
|
@ -1334,6 +1343,113 @@
|
|||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.1.tgz",
|
||||
"integrity": "sha512-nBFhRUb5YzVTCX/iAK1MgQ4uWo89Gu0TH00qQHoYRCsE12dWcG1OiLd7v2EIo2+tpUKPMOQ62QFy9hy9Vg2ULg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/graphql": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.3.1.tgz",
|
||||
"integrity": "sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.3.0",
|
||||
"@octokit/types": "^2.0.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.3.1.tgz",
|
||||
"integrity": "sha512-5/X0AL1ZgoU32fAepTfEoggFinO3rxsMLtzhlUX+RctLrusn/CApJuGFCd0v7GMFhF+8UiCsTTfsu7Fh1HnEJg==",
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^5.5.0",
|
||||
"@octokit/request-error": "^1.0.1",
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/request-error": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.0.tgz",
|
||||
"integrity": "sha512-DNBhROBYjjV/I9n7A8kVkmQNkqFAMem90dSxqvPq57e2hBr7mNTX98y3R2zDpqMQHVRpBDjsvsfIGgBzy+4PAg==",
|
||||
"requires": {
|
||||
"@octokit/types": "^2.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@octokit/rest": {
|
||||
"version": "16.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.37.0.tgz",
|
||||
"integrity": "sha512-qLPK9FOCK4iVpn6ghknNuv/gDDxXQG6+JBQvoCwWjQESyis9uemakjzN36nvvp8SCny7JuzHI2RV8ChbV5mYdQ==",
|
||||
"requires": {
|
||||
"@octokit/request": "^5.2.0",
|
||||
"@octokit/request-error": "^1.0.2",
|
||||
"atob-lite": "^2.0.0",
|
||||
"before-after-hook": "^2.0.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
"deprecation": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.uniq": "^4.5.0",
|
||||
"octokit-pagination-methods": "^1.1.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
|
||||
"integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
|
||||
"requires": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"@pieh/friendly-errors-webpack-plugin": {
|
||||
"version": "1.7.0-chalk-2",
|
||||
"resolved": "https://registry.npmjs.org/@pieh/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.0-chalk-2.tgz",
|
||||
|
|
@ -2094,6 +2210,11 @@
|
|||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
|
||||
},
|
||||
"atob-lite": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz",
|
||||
"integrity": "sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY="
|
||||
},
|
||||
"auto-bind": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-3.0.0.tgz",
|
||||
|
|
@ -2465,6 +2586,11 @@
|
|||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"before-after-hook": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz",
|
||||
"integrity": "sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A=="
|
||||
},
|
||||
"better-assert": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
|
||||
|
|
@ -3041,6 +3167,11 @@
|
|||
"electron-to-chromium": "^1.3.47"
|
||||
}
|
||||
},
|
||||
"btoa-lite": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz",
|
||||
"integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc="
|
||||
},
|
||||
"buffer": {
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
|
||||
|
|
@ -4828,6 +4959,11 @@
|
|||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"des.js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
|
||||
|
|
@ -11092,6 +11228,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
},
|
||||
"lodash.map": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
|
||||
|
|
@ -11107,6 +11248,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
|
||||
|
|
@ -11294,6 +11440,11 @@
|
|||
"yallist": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"macos-release": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz",
|
||||
"integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
|
|
@ -12259,6 +12410,11 @@
|
|||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||
},
|
||||
"octokit-pagination-methods": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz",
|
||||
"integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ=="
|
||||
},
|
||||
"omggif": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
|
||||
|
|
@ -12403,6 +12559,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"os-name": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz",
|
||||
"integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==",
|
||||
"requires": {
|
||||
"macos-release": "^2.2.0",
|
||||
"windows-release": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
|
|
@ -16569,6 +16734,14 @@
|
|||
"crypto-random-string": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"universal-user-agent": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.0.tgz",
|
||||
"integrity": "sha512-eM8knLpev67iBDizr/YtqkJsF3GK8gzDc6st/WKzrTuPtcsOKW/0IdL4cnMBsU69pOx0otavLWBDGTwg+dB0aA==",
|
||||
"requires": {
|
||||
"os-name": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
|
|
@ -17520,6 +17693,30 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"windows-release": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.2.0.tgz",
|
||||
"integrity": "sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==",
|
||||
"requires": {
|
||||
"execa": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"with-open-file": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@actions/core": "^1.2.1",
|
||||
"@actions/exec": "^1.0.3",
|
||||
"@actions/github": "^2.0.1",
|
||||
"@hapi/joi": "^17.0.2",
|
||||
"country-emoji": "^1.5.0",
|
||||
"esm": "^3.2.25",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
const core = require('@actions/core');
|
||||
const { getMasterData, Schema, getStatusCode } = require('./utils.js');
|
||||
const {
|
||||
getMasterData,
|
||||
Schema,
|
||||
getStatusCode,
|
||||
communicateValidationOutcome,
|
||||
} = require('./utils.js');
|
||||
const srcData = require('../src/data.js');
|
||||
|
||||
(async () => {
|
||||
async function main() {
|
||||
// on master branch will be empty array
|
||||
const masterDataUrls = (await getMasterData()).map(d => d.url);
|
||||
// so here data will be an array with all users
|
||||
|
|
@ -18,25 +23,21 @@ const srcData = require('../src/data.js');
|
|||
e.details.forEach(d => core.error(d.message));
|
||||
});
|
||||
|
||||
let failedUrlsCount = 0;
|
||||
const failedUrls = [];
|
||||
for (const { url } of data) {
|
||||
try {
|
||||
const statusCode = await getStatusCode(url);
|
||||
if (statusCode < 200 || statusCode >= 400) {
|
||||
core.error(`Ping to "${url}" failed with status: ${statusCode}`);
|
||||
failedUrlsCount += 1;
|
||||
failedUrls.push(url);
|
||||
}
|
||||
} catch (e) {
|
||||
core.error(`Ping to "${url}" failed with error: ${e}`);
|
||||
failedUrlsCount += 1;
|
||||
failedUrls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (failedUrlsCount) {
|
||||
core.error(`Action failed with ${failedUrlsCount} URL fetch failures`);
|
||||
}
|
||||
await communicateValidationOutcome(errors, failedUrls, data);
|
||||
}
|
||||
|
||||
if (errors.length || failedUrlsCount) {
|
||||
core.setFailed('Action failed with errors, see logs');
|
||||
}
|
||||
})();
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const exec = require('@actions/exec');
|
||||
const core = require('@actions/core');
|
||||
const github = require('@actions/github');
|
||||
const Joi = require('@hapi/joi');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
|
|
@ -84,3 +85,48 @@ module.exports.getStatusCode = function(url) {
|
|||
.on('error', err => reject(err));
|
||||
});
|
||||
};
|
||||
|
||||
// If there are errors, will fail the action & add a comment detailing the issues
|
||||
// If there are no errors, will leave an "all-clear" comment with relevant URLs (to ease a potential manual check)
|
||||
module.exports.communicateValidationOutcome = async function(
|
||||
errors,
|
||||
failedUrls,
|
||||
changedData
|
||||
) {
|
||||
let comment = '';
|
||||
if (errors.length || failedUrls.length) {
|
||||
core.setFailed('Action failed with errors, see logs & comment');
|
||||
|
||||
comment += [
|
||||
'🚨 We have detected the following issues, let us (contributors) know if you need support or clarifications:',
|
||||
...errors.map(e => `- ${e.message}`),
|
||||
...failedUrls.map(url => `- URL is invalid: ${url}`),
|
||||
].join('\n');
|
||||
} else {
|
||||
comment += [
|
||||
'✅ Automatic validation checks succeeded for:',
|
||||
// Comment with the URLs of users that have changed
|
||||
// for easy access, way easier than taking a screenshot
|
||||
...changedData.map(({ name, url }) => `- ${name}, ${url}`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
const { context } = github;
|
||||
if (!GITHUB_TOKEN || !context.payload.pull_request) {
|
||||
core.error(
|
||||
'Cannot add a comment if GITHUB_TOKEN or context.payload.pull_request is not set'
|
||||
);
|
||||
core.info(`Comment contents:\n${comment}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pullRequestNumber = context.payload.pull_request.number;
|
||||
|
||||
const octokit = new github.GitHub(GITHUB_TOKEN);
|
||||
await octokit.issues.createComment({
|
||||
...context.repo,
|
||||
issue_number: pullRequestNumber,
|
||||
body: comment,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1854,8 +1854,8 @@ module.exports = [
|
|||
{
|
||||
name: 'Hugo Di Francesco',
|
||||
description:
|
||||
"JavaScript developer, blogger at codewithhugo.com, author of 'Professional JavaScript' with Packt.",
|
||||
url: 'https://codewithhugo.com/uses/',
|
||||
"JavaScript developer, blogger at codewithhugo.com, co-author of 'Professional JavaScript' with Packt.",
|
||||
url: 'https://codewithhugo.com/uses',
|
||||
twitter: '@hugo__df',
|
||||
emoji: '👓',
|
||||
country: '🇬🇧',
|
||||
|
|
|
|||
Loading…
Reference in a new issue