diff --git a/ops/external-prs/package.json b/ops/external-prs/package.json index 21c643565..4f6e2ff3f 100644 --- a/ops/external-prs/package.json +++ b/ops/external-prs/package.json @@ -27,6 +27,7 @@ }, "keywords": [], "devDependencies": { + "@types/lodash": "^4.17.4", "@types/node": "^20.11.17", "dotenv": "^16.4.1", "ts-node": "^10.9.1", @@ -45,6 +46,7 @@ "duckdb": "^0.10.1", "envfile": "^7.1.0", "libsodium-wrappers": "^0.7.13", + "lodash": "^4.17.21", "mustache": "^4.2.0", "octokit": "^3.1.0", "oss-directory": "^0.0.12", diff --git a/ops/external-prs/src/ossd/index.ts b/ops/external-prs/src/ossd/index.ts index 019b87db9..7417c3fe0 100644 --- a/ops/external-prs/src/ossd/index.ts +++ b/ops/external-prs/src/ossd/index.ts @@ -6,6 +6,7 @@ import { logger } from "../utils/logger.js"; import { BaseArgs, CommmentCommandHandler } from "../base.js"; import { loadData, Project, Collection } from "oss-directory"; import duckdb from "duckdb"; +import _ from "lodash"; import * as util from "util"; import * as fs from "fs"; import * as fsPromise from "fs/promises"; @@ -590,8 +591,38 @@ class OSSDirectoryPullRequest { }); await this.loadValidators(urls); - const validationErrors: { address: string; error: string }[] = []; + // Embedded data structure for storing validation results + type ValidationItem = { + name: string; + messages: string[]; + errors: string[]; + }; + const results: Record = {}; + // Add a name to the results + const ensureNameInResult = (name: string) => { + const item = results[name]; + if (!item) { + results[name] = { + name, + messages: [], + errors: [], + }; + } + }; + // Add an informational message + /** + const addMessageToResult = (name: string, message: string) => { + ensureNameInResult(name); + results[name].messages.push(message); + }; + */ + // Add an error + const addErrorToResult = (name: string, message: string) => { + ensureNameInResult(name); + results[name].errors.push(message); + }; + // Run on-chain validations for (const item of this.changes.artifacts.toValidate.blockchain) { const address = item.address; for (const network of item.networks) { @@ -611,70 +642,58 @@ class OSSDirectoryPullRequest { }); if (item.tags.indexOf("eoa") !== -1) { if (!(await validator.isEOA(address))) { - validationErrors.push({ - address: address, - error: "is not an EOA", - }); + addErrorToResult(address, "is not an EOA"); } } if (item.tags.indexOf("contract") !== -1) { if (!(await validator.isContract(address))) { - validationErrors.push({ - address: address, - error: "is not a Contract", - }); + addErrorToResult(address, "is not a Contract"); } } if (item.tags.indexOf("deployer") !== -1) { if (!(await validator.isDeployer(address))) { - validationErrors.push({ - address: address, - error: "is not a Deployer", - }); + addErrorToResult(address, "is not a Deployer"); } } } } - if (validationErrors.length !== 0) { - logger.info({ - message: "found validation errors", - count: validationErrors.length, - }); + // Summarize results + const items: ValidationItem[] = _.values(results); + const numErrors = _.sumBy( + items, + (item: ValidationItem) => item.errors.length, + ); + const summaryMessage = + numErrors > 0 + ? `⛔ Found ${numErrors} errors ⛔` + : items.length > 0 + ? "⚠️ Please review validation items before approving ⚠️" + : "✅ Good to go as long as status checks pass"; + const commentBody = await renderMustacheFromFile( + relativeDir("messages", "validation-message.md"), + { + sha: args.sha, + summaryMessage, + validationItems: items, + }, + ); - await args.appUtils.setStatusComment( - args.pr, - await renderMustacheFromFile( - relativeDir("messages", "validation-errors.md"), - { - validationErrors: validationErrors, - sha: args.sha, - }, - ), - ); - - await args.appUtils.setCheckStatus({ - conclusion: CheckConclusion.Failure, - name: "validate", - head_sha: args.sha, - status: CheckStatus.Completed, - output: { - title: "PR Validation", - summary: `Failed to validate with ${validationErrors.length} errors`, - }, - }); - } else { - await args.appUtils.setCheckStatus({ - conclusion: CheckConclusion.Success, - name: "validate", - head_sha: args.sha, - status: CheckStatus.Completed, - output: { - title: "PR Validation", - summary: "Successfully validated", - }, - }); - } + // Update the PR comment + await args.appUtils.setStatusComment(args.pr, commentBody); + // Update the PR status + await args.appUtils.setCheckStatus({ + conclusion: + numErrors > 0 ? CheckConclusion.Failure : CheckConclusion.Success, + name: "validate", + head_sha: args.sha, + status: CheckStatus.Completed, + output: { + title: + numErrors > 0 ? summaryMessage : "Successfully validated all items", + summary: commentBody, + }, + }); } } diff --git a/ops/external-prs/src/ossd/messages/validation-errors.md b/ops/external-prs/src/ossd/messages/validation-errors.md deleted file mode 100644 index 645335799..000000000 --- a/ops/external-prs/src/ossd/messages/validation-errors.md +++ /dev/null @@ -1,6 +0,0 @@ -Validation of commit `{{sha}}` failed with the following errors: - -{{#validationErrors}} - -- `{{address}}` {{error}} - {{/validationErrors}} diff --git a/ops/external-prs/src/ossd/messages/validation-message.md b/ops/external-prs/src/ossd/messages/validation-message.md new file mode 100644 index 000000000..a4b4fec34 --- /dev/null +++ b/ops/external-prs/src/ossd/messages/validation-message.md @@ -0,0 +1,23 @@ +## Validation Results + +{{summaryMessage}} + +commit `{{sha}}` + +--- + +{{#validationItems}} + +### {{name}} + +{{#errors}} + +- ❌ {{.}} + {{/errors}} + +{{#messages}} + +- 👉 {{.}} + {{/messages}} + +{{/validationItems}} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee2c0fb06..a1a9f3477 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -378,6 +378,9 @@ importers: libsodium-wrappers: specifier: ^0.7.13 version: 0.7.13 + lodash: + specifier: ^4.17.21 + version: 4.17.21 mustache: specifier: ^4.2.0 version: 4.2.0 @@ -403,6 +406,9 @@ importers: specifier: ^17.7.2 version: 17.7.2 devDependencies: + '@types/lodash': + specifier: ^4.17.4 + version: 4.17.4 '@types/node': specifier: ^20.11.17 version: 20.11.17 @@ -4235,6 +4241,9 @@ packages: '@types/lodash@4.17.0': resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + '@types/lodash@4.17.4': + resolution: {integrity: sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==} + '@types/lru-cache@5.1.1': resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} @@ -17353,7 +17362,7 @@ snapshots: '@types/hoist-non-react-statics@3.3.5': dependencies: - '@types/react': 18.2.48 + '@types/react': 18.3.3 hoist-non-react-statics: 3.3.2 '@types/html-minifier-terser@6.1.0': {} @@ -17411,6 +17420,8 @@ snapshots: '@types/lodash@4.17.0': {} + '@types/lodash@4.17.4': {} + '@types/lru-cache@5.1.1': {} '@types/luxon@3.3.1': {} @@ -17496,11 +17507,11 @@ snapshots: '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 18.2.64 + '@types/react': 18.3.3 '@types/react-transition-group@4.4.10': dependencies: - '@types/react': 18.2.48 + '@types/react': 18.3.3 '@types/react@18.2.48': dependencies: