From f7027e759df1ef852c95840e39f2467cc95c0292 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Mon, 11 May 2026 15:45:51 -0300 Subject: [PATCH] feat: check for reports without PR_URL on H1 --- lib/cli.js | 8 +++++ lib/prepare_security.js | 68 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 1c119b75..dfb6cc75 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -94,6 +94,14 @@ export default class CLI { return answer; } + async promptCheckbox(message, choices) { + if (this.assumeYes) { + return choices.filter((c) => c.checked).map((c) => c.value); + } + + return inquirer.checkbox({ message, choices }); + } + setAssumeYes() { this.assumeYes = true; } diff --git a/lib/prepare_security.js b/lib/prepare_security.js index 943bc60b..cf7d2890 100644 --- a/lib/prepare_security.js +++ b/lib/prepare_security.js @@ -10,11 +10,19 @@ import { validateDate, promptDependencies, getSupportedVersions, + getReportSeverity, pickReport, SecurityRelease } from './security-release/security-release.js'; import _ from 'lodash'; +function relativeDate(date) { + const days = Math.floor((Date.now() - date) / (1000 * 60 * 60 * 24)); + if (days < 30) return days === 1 ? '1 day ago' : `${days} days ago`; + const months = Math.floor(days / 30); + return months === 1 ? '1 month ago' : `${months} months ago`; +} + export default class PrepareSecurityRelease extends SecurityRelease { title = 'Next Security Release'; @@ -25,6 +33,13 @@ export default class PrepareSecurityRelease extends SecurityRelease { }); this.req = new Request(credentials); + + let excludedReports = []; + const showTriaged = await this.promptShowTriagedWithoutPR(); + if (showTriaged) { + excludedReports = await this.showTriagedReportsWithoutPR(); + } + const releaseDate = await this.promptReleaseDate(); if (releaseDate !== 'TBD') { validateDate(releaseDate); @@ -34,7 +49,8 @@ export default class PrepareSecurityRelease extends SecurityRelease { let securityReleasePRUrl; const content = await this.buildDescription(releaseDate, securityReleasePRUrl); if (createVulnerabilitiesJSON) { - securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate, content); + securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation( + releaseDate, content, excludedReports); } this.cli.ok('Done!'); @@ -93,12 +109,12 @@ export default class PrepareSecurityRelease extends SecurityRelease { this.cli.ok('Done!'); } - async startVulnerabilitiesJSONCreation(releaseDate, content) { + async startVulnerabilitiesJSONCreation(releaseDate, content, excludedReports = []) { // checkout on the next-security-release branch checkoutOnSecurityReleaseBranch(this.cli, this.repository); // choose the reports to include in the security release - const reports = await this.chooseReports(); + const reports = await this.chooseReports(excludedReports); const depUpdates = await this.getDependencyUpdates(); const deps = _.groupBy(depUpdates, 'name'); @@ -184,17 +200,61 @@ export default class PrepareSecurityRelease extends SecurityRelease { { defaultAnswer: true }); } + async promptShowTriagedWithoutPR() { + return this.cli.prompt( + 'Do you want to see which reports are triaged but have no PR URL?', + { defaultAnswer: true }); + } + + async showTriagedReportsWithoutPR() { + this.cli.info('Fetching triaged reports without PR URL...'); + const reports = await this.req.getTriagedReports(); + const reportsWithoutPR = reports.data.filter( + (report) => !report.relationships.custom_field_values.data.length + ); + if (!reportsWithoutPR.length) { + this.cli.ok('All triaged reports have a PR URL.'); + return []; + } + const severityRank = { critical: 0, high: 1, medium: 2, low: 3 }; + const choices = reportsWithoutPR + .sort((a, b) => { + const dateA = new Date(a.attributes.created_at); + const dateB = new Date(b.attributes.created_at); + if (dateB - dateA !== 0) return dateB - dateA; + const rankA = severityRank[getReportSeverity(a).rating] ?? 4; + const rankB = severityRank[getReportSeverity(b).rating] ?? 4; + return rankA - rankB; + }) + .map((report) => { + const { id, attributes: { title, created_at } } = report; + const { rating } = getReportSeverity(report); + const openedDate = relativeDate(new Date(created_at)); + const link = `https://hackerone.com/reports/${id}`; + return { + name: `[${openedDate}] (${rating}) ${title} - ${link}`, + value: id, + checked: true + }; + }); + return this.cli.promptCheckbox( + 'Select reports to exclude from the upcoming security release:', + choices + ); + } + async buildDescription() { const template = await this.getSecurityIssueTemplate(); return template; } - async chooseReports() { + async chooseReports(excludedReports = []) { this.cli.info('Getting triaged H1 reports...'); const reports = await this.req.getTriagedReports(); const selectedReports = []; for (const report of reports.data) { + if (excludedReports.includes(report.id)) continue; const rep = await pickReport(report, { cli: this.cli, req: this.req }); if (!rep) continue; selectedReports.push(rep);