diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 87d1b185..5fa54aa8 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,16 +5,20 @@ on: branches: - main +permissions: + contents: read + jobs: release-please: runs-on: ubuntu-latest outputs: release_created: ${{ steps.release.outputs.release_created }} steps: - - uses: google-github-actions/release-please-action@v4 + - uses: googleapis/release-please-action@v4 id: release with: release-type: node + token: ${{ secrets.RELEASE_PLEASE_GITHUB_TOKEN }} npm-publish: needs: release-please if: ${{ needs.release-please.outputs.release_created }} diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1d8b11..b543292f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [5.5.0](https://github.com/nodejs/node-core-utils/compare/v5.4.0...v5.5.0) (2024-09-01) + + +### Features + +* add git node security --cleanup ([#833](https://github.com/nodejs/node-core-utils/issues/833)) ([871a16f](https://github.com/nodejs/node-core-utils/commit/871a16f7968d112468f751ab019ca575151745d2)) +* add warning when fail on all platforms ([#843](https://github.com/nodejs/node-core-utils/issues/843)) ([4f7ec3e](https://github.com/nodejs/node-core-utils/commit/4f7ec3e7923592dbfef32206bf1c2b21a36e15dd)) +* **git-node:** auto-fetch comparison branch when preparing release ([#846](https://github.com/nodejs/node-core-utils/issues/846)) ([a8529ed](https://github.com/nodejs/node-core-utils/commit/a8529edaab765e293965054b7027774a3f4938f2)) +* **git-node:** auto-fetch latest release tag when preparing release ([#842](https://github.com/nodejs/node-core-utils/issues/842)) ([15ae401](https://github.com/nodejs/node-core-utils/commit/15ae4013b2cbf56ec7f3356d8a8bb682ceef29c3)) +* update next-security-release folder on cleanup ([#840](https://github.com/nodejs/node-core-utils/issues/840)) ([f420432](https://github.com/nodejs/node-core-utils/commit/f420432c6cbdba51da734276d20f73c75728b409)) +* **v8:** add fast_float to V8 deps ([#844](https://github.com/nodejs/node-core-utils/issues/844)) ([4e8ec9c](https://github.com/nodejs/node-core-utils/commit/4e8ec9c072283c8cb31155c66815027ed82fa210)) + + +### Bug Fixes + +* landing session on different repo/org ([#847](https://github.com/nodejs/node-core-utils/issues/847)) ([7734954](https://github.com/nodejs/node-core-utils/commit/77349540beee5c8d8808f5783e72e106b02f113b)) + ## [5.4.0](https://github.com/nodejs/node-core-utils/compare/v5.3.1...v5.4.0) (2024-08-07) diff --git a/components/git/release.js b/components/git/release.js index c74ba5ab..ae82e0ae 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -78,6 +78,8 @@ async function main(state, argv, cli, dir) { if (state === PREPARE) { const prep = new ReleasePreparation(argv, cli, dir); + await prep.prepareLocalBranch(); + if (prep.warnForWrongBranch()) return; // If the new version was automatically calculated, confirm it. diff --git a/components/git/security.js b/components/git/security.js index cdb4771a..bd306f1d 100644 --- a/components/git/security.js +++ b/components/git/security.js @@ -43,6 +43,10 @@ const securityOptions = { 'post-release': { describe: 'Create the post-release announcement', type: 'boolean' + }, + cleanup: { + describe: 'cleanup the security release.', + type: 'boolean' } }; @@ -81,6 +85,9 @@ export function builder(yargs) { ).example( 'git node security --post-release', 'Create the post-release announcement on the Nodejs.org repo' + ).example( + 'git node security --cleanup', + 'Cleanup the security release. Merge the PR and close H1 reports' ); } @@ -112,6 +119,9 @@ export function handler(argv) { if (argv['post-release']) { return createPostRelease(argv); } + if (argv.cleanup) { + return cleanupSecurityRelease(argv); + } yargsInstance.showHelp(); } @@ -167,6 +177,13 @@ async function startSecurityRelease() { return release.start(); } +async function cleanupSecurityRelease() { + const logStream = process.stdout.isTTY ? process.stdout : process.stderr; + const cli = new CLI(logStream); + const release = new PrepareSecurityRelease(cli); + return release.cleanup(); +} + async function syncSecurityRelease(argv) { const logStream = process.stdout.isTTY ? process.stdout : process.stderr; const cli = new CLI(logStream); diff --git a/lib/ci/build-types/citgm_comparison_build.js b/lib/ci/build-types/citgm_comparison_build.js index fb6c6775..d5fb58a8 100644 --- a/lib/ci/build-types/citgm_comparison_build.js +++ b/lib/ci/build-types/citgm_comparison_build.js @@ -44,6 +44,7 @@ export class CITGMComparisonBuild { const { failures: comparisonFailures } = comparisonBuild.results; const failures = {}; + let allPlatformFailures; for (const platform in comparisonFailures) { // Account for no failure on this platform, or different platform. if (!Object.prototype.hasOwnProperty.call(baseFailures, platform)) { @@ -66,11 +67,18 @@ export class CITGMComparisonBuild { if (newFailures.length !== 0) { result = statusType.FAILURE; } - + if (allPlatformFailures === undefined) { + allPlatformFailures = newFailures; + } else if (allPlatformFailures.length > 0) { + allPlatformFailures = allPlatformFailures.filter(f => { + return newFailures.includes(f); + }); + } failures[platform] = newFailures; } this.results.failures = failures; + this.results.allPlatformFailures = allPlatformFailures; this.result = result; return result; @@ -124,6 +132,12 @@ export class CITGMComparisonBuild { const str = `${totalFailures} failures in ${cID} not present in ${bID}`; cli.log(`${statusType.FAILURE}: ${str}\n\n`); console.table(output); + if ( + results.allPlatformFailures && + results.allPlatformFailures.length) { + const failures = results.allPlatformFailures.join(', '); + console.warn(`These modules failed in all platforms: ${failures}`); + } } formatAsJson() { diff --git a/lib/landing_session.js b/lib/landing_session.js index 9deb8cb9..7c86efc5 100644 --- a/lib/landing_session.js +++ b/lib/landing_session.js @@ -21,7 +21,7 @@ export default class LandingSession extends Session { prid, backport, lint, autorebase, fixupAll, checkCI, oneCommitMax, ...argv } = {}) { - super(cli, dir, prid); + super(cli, dir, prid, argv); this.req = req; this.backport = backport; this.lint = lint; diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 5b64d79f..6b0aa131 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -4,8 +4,7 @@ import { promises as fs } from 'node:fs'; import semver from 'semver'; import { replaceInFile } from 'replace-in-file'; -import { getMergedConfig } from './config.js'; -import { runAsync, runSync } from './run.js'; +import { forceRunAsync, runAsync, runSync } from './run.js'; import { writeJson, readJson } from './file.js'; import Request from './request.js'; import auth from './auth.js'; @@ -15,58 +14,25 @@ import { updateTestProcessRelease } from './release/utils.js'; import CherryPick from './cherry_pick.js'; +import Session from './session.js'; const isWindows = process.platform === 'win32'; -export default class ReleasePreparation { +export default class ReleasePreparation extends Session { constructor(argv, cli, dir) { - this.cli = cli; - this.dir = dir; + super(cli, dir); this.isSecurityRelease = argv.security; this.isLTS = false; this.isLTSTransition = argv.startLTS; this.runBranchDiff = !argv.skipBranchDiff; this.ltsCodename = ''; this.date = ''; - this.config = getMergedConfig(this.dir); this.filterLabels = argv.filterLabel && argv.filterLabel.split(','); + this.newVersion = argv.newVersion; + } - // Ensure the preparer has set an upstream and username. - if (this.warnForMissing()) { - cli.error('Failed to begin the release preparation process.'); - return; - } - - // Allow passing optional new version. - if (argv.newVersion) { - const newVersion = semver.clean(argv.newVersion); - if (!semver.valid(newVersion)) { - cli.warn(`${newVersion} is not a valid semantic version.`); - return; - } - this.newVersion = newVersion; - } else { - this.newVersion = this.calculateNewVersion(); - } - - const { upstream, owner, repo, newVersion } = this; - - this.versionComponents = { - major: semver.major(newVersion), - minor: semver.minor(newVersion), - patch: semver.patch(newVersion) - }; - - this.stagingBranch = `v${this.versionComponents.major}.x-staging`; - this.releaseBranch = `v${this.versionComponents.major}.x`; - - const upstreamHref = runSync('git', [ - 'config', '--get', - `remote.${upstream}.url`]).trim(); - if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) { - cli.warn('Remote repository URL does not point to the expected ' + - `repository ${owner}/${repo}`); - } + get branch() { + return this.stagingBranch; } warnForNonMergeablePR(pr) { @@ -205,7 +171,7 @@ export default class ReleasePreparation { // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); - const raw = this.getBranchDiff({ + const raw = await this.getBranchDiff({ onlyNotableChanges: false, comparisonBranch: newVersion }); @@ -215,10 +181,9 @@ export default class ReleasePreparation { const outstandingCommits = diff.length - 1; if (outstandingCommits !== 0) { - const staging = `v${semver.major(newVersion)}.x-staging`; const proceed = await cli.prompt( `There are ${outstandingCommits} commits that may be ` + - `backported to ${staging} - do you still want to proceed?`, + `backported to ${this.stagingBranch} - do you still want to proceed?`, { defaultAnswer: false }); if (!proceed) { @@ -369,24 +334,19 @@ export default class ReleasePreparation { return missing; } - calculateNewVersion() { - let newVersion; - - const lastTagVersion = semver.clean(this.getLastRef()); - const lastTag = { - major: semver.major(lastTagVersion), - minor: semver.minor(lastTagVersion), - patch: semver.patch(lastTagVersion) - }; - - const changelog = this.getChangelog(); + async calculateNewVersion({ tagName, major, minor, patch }) { + const changelog = this.getChangelog(tagName); + const newVersion = { major, minor, patch }; if (changelog.includes('SEMVER-MAJOR')) { - newVersion = `${lastTag.major + 1}.0.0`; + newVersion.major++; + newVersion.minor = 0; + newVersion.patch = 0; } else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) { - newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`; + newVersion.minor++; + newVersion.patch = 0; } else { - newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`; + newVersion.patch++; } return newVersion; @@ -396,11 +356,22 @@ export default class ReleasePreparation { return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); } - getLastRef() { - return runSync('git', ['describe', '--abbrev=0', '--tags']).trim(); + getLastRef(tagName) { + if (!tagName) { + return runSync('git', ['describe', '--abbrev=0', '--tags']).trim(); + } + + try { + runSync('git', ['rev-parse', tagName]); + } catch { + this.cli.startSpinner(`Error parsing git ref ${tagName}, attempting fetching it as a tag`); + runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]); + this.cli.stopSpinner(`Tag fetched: ${tagName}`); + } + return tagName; } - getChangelog() { + getChangelog(tagName) { const changelogMaker = new URL( '../node_modules/.bin/changelog-maker' + (isWindows ? '.cmd' : ''), import.meta.url @@ -411,7 +382,7 @@ export default class ReleasePreparation { '--markdown', '--filter-release', '--start-ref', - this.getLastRef() + this.getLastRef(tagName) ]).trim(); } @@ -496,7 +467,7 @@ export default class ReleasePreparation { const data = await fs.readFile(majorChangelogPath, 'utf8'); const arr = data.split('\n'); const allCommits = this.getChangelog(); - const notableChanges = this.getBranchDiff({ onlyNotableChanges: true }); + const notableChanges = await this.getBranchDiff({ onlyNotableChanges: true }); let releaseHeader = `## ${date}, Version ${newVersion}` + ` ${releaseInfo}, @${username}\n`; if (isSecurityRelease) { @@ -550,14 +521,14 @@ export default class ReleasePreparation { } async createProposalBranch(base = this.stagingBranch) { - const { upstream, newVersion } = this; + const { newVersion } = this; const proposalBranch = `v${newVersion}-proposal`; await runAsync('git', [ 'checkout', '-b', proposalBranch, - `${upstream}/${base}` + base ]); return proposalBranch; } @@ -632,7 +603,7 @@ export default class ReleasePreparation { messageBody.push('This is a security release.\n\n'); } - const notableChanges = this.getBranchDiff({ + const notableChanges = await this.getBranchDiff({ onlyNotableChanges: true, format: 'plaintext' }); @@ -659,8 +630,9 @@ export default class ReleasePreparation { return useMessage; } - getBranchDiff(opts) { + async getBranchDiff(opts) { const { + cli, versionComponents = {}, upstream, newVersion, @@ -688,6 +660,10 @@ export default class ReleasePreparation { 'semver-minor' ]; + await forceRunAsync('git', ['remote', 'set-branches', '--add', upstream, releaseBranch], { + ignoreFailures: false + }); + await forceRunAsync('git', ['fetch', upstream, releaseBranch], { ignoreFailures: false }); branchDiffOptions = [ `${upstream}/${releaseBranch}`, proposalBranch, @@ -706,20 +682,43 @@ export default class ReleasePreparation { 'baking-for-lts' ]; - let comparisonBranch = 'main'; + let comparisonBranch = this.config.branch || 'main'; const isSemverMinor = versionComponents.patch === 0; if (isLTS) { + const res = await fetch('https://nodejs.org/dist/index.json'); + if (!res.ok) throw new Error('Failed to fetch', { cause: res }); + const [latest] = await res.json(); // Assume Current branch matches tag with highest semver value. - const tags = runSync('git', - ['tag', '-l', '--sort', '-version:refname']).trim(); - const highestVersionTag = tags.split('\n')[0]; - comparisonBranch = `v${semver.coerce(highestVersionTag).major}.x`; + comparisonBranch = `v${semver.coerce(latest.version).major}.x`; if (!isSemverMinor) { excludeLabels.push('semver-minor'); } } + await forceRunAsync('git', ['fetch', upstream, comparisonBranch], { ignoreFailures: false }); + const commits = await forceRunAsync('git', ['rev-parse', 'FETCH_HEAD', comparisonBranch], { + captureStdout: 'lines', + ignoreFailures: true + }); + if (commits == null) { + const shouldCreateCompareBranch = await cli.prompt( + `No local branch ${comparisonBranch}, do you want to create it?`); + if (shouldCreateCompareBranch) { + await forceRunAsync('git', ['branch', comparisonBranch, 'FETCH_HEAD'], { + ignoreFailures: false + }); + } + } else if (commits[0] !== commits[1]) { + const shouldUpBranch = cli.prompt(`Local ${comparisonBranch} branch is not in sync with ${ + upstream}/${comparisonBranch}, do you want to update it?`); + if (shouldUpBranch) { + await forceRunAsync('git', ['branch', '-f', comparisonBranch, 'FETCH_HEAD'], { + ignoreFailures: false + }); + } + } + branchDiffOptions = [ stagingBranch, comparisonBranch, @@ -736,6 +735,67 @@ export default class ReleasePreparation { return runSync(branchDiff, branchDiffOptions); } + async getLastRelease(major) { + const { cli } = this; + + cli.startSpinner(`Parsing CHANGELOG for most recent release of v${major}.x`); + const data = await fs.readFile( + path.resolve(`doc/changelogs/CHANGELOG_V${major}.md`), + 'utf8' + ); + const [,, minor, patch] = /\1\.\2\.\3<\/a>/.exec(data); + this.isLTS = data.includes('LTS '); + + cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}${ + this.isLTS ? ' (LTS)' : '' + }`); + + return { + tagName: await this.getLastRef(`v${major}.${minor}.${patch}`), + major, minor, patch + }; + } + + async prepareLocalBranch() { + const { cli } = this; + if (this.newVersion) { + // If the CLI asked for a specific version: + const newVersion = semver.parse(this.newVersion); + if (!newVersion) { + cli.warn(`${this.newVersion} is not a valid semantic version.`); + return; + } + this.newVersion = newVersion.version; + this.versionComponents = { + major: newVersion.major, + minor: newVersion.minor, + patch: newVersion.patch + }; + this.stagingBranch = `v${newVersion.major}.x-staging`; + this.releaseBranch = `v${newVersion.major}.x`; + await this.tryResetBranch(); + await this.getLastRelease(newVersion.major); + return; + } + + // Otherwise, we need to figure out what's the next version number for the + // release line of the branch that's currently checked out. + const currentBranch = this.getCurrentBranch(); + const match = /^v(\d+)\.x-staging$/.exec(currentBranch); + + if (!match) { + cli.warn(`Cannot prepare a release from ${currentBranch + }. Switch to a staging branch before proceeding.`); + return; + } + this.stagingBranch = currentBranch; + await this.tryResetBranch(); + this.versionComponents = await this.calculateNewVersion(await this.getLastRelease(match[1])); + const { major, minor, patch } = this.versionComponents; + this.newVersion = `${major}.${minor}.${patch}`; + this.releaseBranch = `v${major}.x`; + } + warnForWrongBranch() { const { cli, diff --git a/lib/prepare_security.js b/lib/prepare_security.js index 4ffb90fe..8d4ba962 100644 --- a/lib/prepare_security.js +++ b/lib/prepare_security.js @@ -5,22 +5,18 @@ import Request from './request.js'; import { NEXT_SECURITY_RELEASE_BRANCH, NEXT_SECURITY_RELEASE_FOLDER, - NEXT_SECURITY_RELEASE_REPOSITORY, checkoutOnSecurityReleaseBranch, commitAndPushVulnerabilitiesJSON, validateDate, promptDependencies, getSupportedVersions, - pickReport + pickReport, + SecurityRelease } from './security-release/security-release.js'; import _ from 'lodash'; -export default class PrepareSecurityRelease { - repository = NEXT_SECURITY_RELEASE_REPOSITORY; +export default class PrepareSecurityRelease extends SecurityRelease { title = 'Next Security Release'; - constructor(cli) { - this.cli = cli; - } async start() { const credentials = await auth({ @@ -44,6 +40,40 @@ export default class PrepareSecurityRelease { this.cli.ok('Done!'); } + async cleanup() { + const credentials = await auth({ + github: true, + h1: true + }); + + this.req = new Request(credentials); + const vulnerabilityJSON = this.readVulnerabilitiesJSON(); + this.cli.info('Closing and request disclosure to HackerOne reports'); + await this.closeAndRequestDisclosure(vulnerabilityJSON.reports); + + this.cli.info('Closing pull requests'); + // For now, close the ones with vN.x label + await this.closePRWithLabel(this.getAffectedVersions(vulnerabilityJSON)); + + const updateFolder = this.cli.prompt( + // eslint-disable-next-line max-len + `Would you like to update the next-security-release folder to ${vulnerabilityJSON.releaseDate}?`, + { defaultAnswer: true }); + if (updateFolder) { + const newFolder = this.updateReleaseFolder(vulnerabilityJSON.releaseDate); + commitAndPushVulnerabilitiesJSON( + newFolder, + 'chore: change next-security-release folder', + { cli: this.cli, repository: this.repository } + ); + } + this.cli.info(`Merge pull request with: + - git checkout main + - git merge --squash ${NEXT_SECURITY_RELEASE_BRANCH} + - git push origin main`); + this.cli.ok('Done!'); + } + async startVulnerabilitiesJSONCreation(releaseDate, content) { // checkout on the next-security-release branch checkoutOnSecurityReleaseBranch(this.cli, this.repository); @@ -163,9 +193,9 @@ export default class PrepareSecurityRelease { const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER); try { - await fs.accessSync(folderPath); + fs.accessSync(folderPath); } catch (error) { - await fs.mkdirSync(folderPath, { recursive: true }); + fs.mkdirSync(folderPath, { recursive: true }); } const fullPath = path.join(folderPath, 'vulnerabilities.json'); @@ -254,4 +284,38 @@ export default class PrepareSecurityRelease { } return deps; } + + async closeAndRequestDisclosure(jsonReports) { + this.cli.startSpinner('Closing HackerOne reports'); + for (const report of jsonReports) { + this.cli.updateSpinner(`Closing report ${report.id}...`); + await this.req.updateReportState( + report.id, + 'resolved', + 'Closing as resolved' + ); + + this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`); + await this.req.requestDisclosure(report.id); + } + this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure'); + } + + async closePRWithLabel(labels) { + if (typeof labels === 'string') { + labels = [labels]; + } + + const url = 'https://github.com/nodejs-private/node-private/pulls'; + this.cli.startSpinner('Closing GitHub Pull Requests...'); + // At this point, GitHub does not provide filters through their REST API + const prs = this.req.getPullRequest(url); + for (const pr of prs) { + if (pr.labels.some((l) => labels.includes(l))) { + this.cli.updateSpinner(`Closing Pull Request: ${pr.id}`); + await this.req.closePullRequest(pr.id); + } + } + this.cli.startSpinner('Closed GitHub Pull Requests.'); + } } diff --git a/lib/request.js b/lib/request.js index 553322f3..a4b43586 100644 --- a/lib/request.js +++ b/lib/request.js @@ -109,6 +109,22 @@ export default class Request { return this.json(url, options); } + async closePullRequest({ owner, repo }) { + const url = `https://api.github.com/repos/${owner}/${repo}/pulls`; + const options = { + method: 'POST', + headers: { + Authorization: `Basic ${this.credentials.github}`, + 'User-Agent': 'node-core-utils', + Accept: 'application/vnd.github+json' + }, + body: JSON.stringify({ + state: 'closed' + }) + }; + return this.json(url, options); + } + async gql(name, variables, path) { const query = this.loadQuery(name); if (path) { @@ -201,6 +217,49 @@ export default class Request { return this.json(url, options); } + async updateReportState(reportId, state, message) { + const url = `https://api.hackerone.com/v1/reports/${reportId}/state_changes`; + const options = { + method: 'POST', + headers: { + Authorization: `Basic ${this.credentials.h1}`, + 'User-Agent': 'node-core-utils', + Accept: 'application/json' + }, + body: JSON.stringify({ + data: { + type: 'state-change', + attributes: { + message, + state + } + } + }) + }; + return this.json(url, options); + } + + async requestDisclosure(reportId) { + const url = `https://api.hackerone.com/v1/reports/${reportId}/disclosure_requests`; + const options = { + method: 'POST', + headers: { + Authorization: `Basic ${this.credentials.h1}`, + 'User-Agent': 'node-core-utils', + Accept: 'application/json' + }, + body: JSON.stringify({ + data: { + attributes: { + // default to limited version + substate: 'no-content' + } + } + }) + }; + return this.json(url, options); + } + // This is for github v4 API queries, for other types of queries // use .text or .json async query(query, variables) { diff --git a/lib/security-release/security-release.js b/lib/security-release/security-release.js index 73f93cd6..3a4482a7 100644 --- a/lib/security-release/security-release.js +++ b/lib/security-release/security-release.js @@ -210,3 +210,64 @@ export async function pickReport(report, { cli, req }) { reporter: reporter.data.attributes.username }; } + +export class SecurityRelease { + constructor(cli, repository = NEXT_SECURITY_RELEASE_REPOSITORY) { + this.cli = cli; + this.repository = repository; + } + + readVulnerabilitiesJSON(vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath()) { + const exists = fs.existsSync(vulnerabilitiesJSONPath); + + if (!exists) { + this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`); + process.exit(1); + } + + return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8')); + } + + getVulnerabilitiesJSONPath() { + return path.join(process.cwd(), + NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json'); + } + + updateReleaseFolder(releaseDate) { + const folder = path.join(process.cwd(), + NEXT_SECURITY_RELEASE_FOLDER); + const newFolder = path.join(process.cwd(), releaseDate); + fs.renameSync(folder, newFolder); + return newFolder; + } + + updateVulnerabilitiesJSON(content) { + try { + const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); + this.cli.startSpinner(`Updating vulnerabilities.json from ${vulnerabilitiesJSONPath}...`); + fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); + commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, + 'chore: updated vulnerabilities.json', + { cli: this.cli, repository: this.repository }); + this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`); + } catch (error) { + this.cli.error('Error updating vulnerabilities.json'); + this.cli.error(error); + } + } + + getAffectedVersions(content) { + const affectedVersions = new Set(); + for (const report of Object.values(content.reports)) { + for (const affectedVersion of report.affectedVersions) { + affectedVersions.add(affectedVersion); + } + } + const parseToNumber = str => +(str.match(/[\d.]+/g)[0]); + return Array.from(affectedVersions) + .sort((a, b) => { + return parseToNumber(a) > parseToNumber(b) ? -1 : 1; + }) + .join(', '); + } +} diff --git a/lib/security_blog.js b/lib/security_blog.js index c0987bfe..34251d4d 100644 --- a/lib/security_blog.js +++ b/lib/security_blog.js @@ -4,24 +4,17 @@ import _ from 'lodash'; import nv from '@pkgjs/nv'; import { PLACEHOLDERS, - getVulnerabilitiesJSON, checkoutOnSecurityReleaseBranch, - NEXT_SECURITY_RELEASE_REPOSITORY, validateDate, - commitAndPushVulnerabilitiesJSON, - NEXT_SECURITY_RELEASE_FOLDER + SecurityRelease } from './security-release/security-release.js'; import auth from './auth.js'; import Request from './request.js'; const kChanged = Symbol('changed'); -export default class SecurityBlog { - repository = NEXT_SECURITY_RELEASE_REPOSITORY; +export default class SecurityBlog extends SecurityRelease { req; - constructor(cli) { - this.cli = cli; - } async createPreRelease() { const { cli } = this; @@ -30,7 +23,7 @@ export default class SecurityBlog { checkoutOnSecurityReleaseBranch(cli, this.repository); // read vulnerabilities JSON file - const content = getVulnerabilitiesJSON(cli); + const content = this.readVulnerabilitiesJSON(); // validate the release date read from vulnerabilities JSON if (!content.releaseDate) { cli.error('Release date is not set in vulnerabilities.json,' + @@ -72,7 +65,7 @@ export default class SecurityBlog { checkoutOnSecurityReleaseBranch(cli, this.repository); // read vulnerabilities JSON file - const content = getVulnerabilitiesJSON(cli); + const content = this.readVulnerabilitiesJSON(cli); if (!content.releaseDate) { cli.error('Release date is not set in vulnerabilities.json,' + ' run `git node security --update-date=YYYY/MM/DD` to set the release date.'); @@ -113,22 +106,6 @@ export default class SecurityBlog { this.updateVulnerabilitiesJSON(content); } - updateVulnerabilitiesJSON(content) { - try { - this.cli.info('Updating vulnerabilities.json'); - const vulnerabilitiesJSONPath = path.join(process.cwd(), - NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json'); - fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); - const commitMessage = 'chore: updated vulnerabilities.json'; - commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath, - commitMessage, - { cli: this.cli, repository: this.repository }); - } catch (error) { - this.cli.error('Error updating vulnerabilities.json'); - this.cli.error(error); - } - } - async promptExistingPreRelease(cli) { const pathPreRelease = await cli.prompt( 'Please provide the path of the existing pre-release announcement:', { @@ -324,21 +301,6 @@ export default class SecurityBlog { return text.join('\n'); } - getAffectedVersions(content) { - const affectedVersions = new Set(); - for (const report of Object.values(content.reports)) { - for (const affectedVersion of report.affectedVersions) { - affectedVersions.add(affectedVersion); - } - } - const parseToNumber = str => +(str.match(/[\d.]+/g)[0]); - return Array.from(affectedVersions) - .sort((a, b) => { - return parseToNumber(a) > parseToNumber(b) ? -1 : 1; - }) - .join(', '); - } - getSecurityPreReleaseTemplate() { return fs.readFileSync( new URL( diff --git a/lib/update-v8/constants.js b/lib/update-v8/constants.js index 31a99635..d8ed501f 100644 --- a/lib/update-v8/constants.js +++ b/lib/update-v8/constants.js @@ -38,6 +38,9 @@ const fp16Ignore = `!/third_party/fp16 /third_party/fp16/src/* !/third_party/fp16/src/include`; +const fastFloatReplace = `/third_party/fast_float/src/* +!/third_party/fast_float/src/include`; + export const v8Deps = [ { name: 'trace_event', @@ -103,5 +106,14 @@ export const v8Deps = [ repo: 'third_party/fp16/src', gitignore: fp16Ignore, since: 124 + }, + { + name: 'fast_float', + repo: 'third_party/fast_float/src', + gitignore: { + match: '/third_party/fast_float/src', + replace: fastFloatReplace + }, + since: 130 } ]; diff --git a/lib/update_security_release.js b/lib/update_security_release.js index c9ae2dd4..66e2c162 100644 --- a/lib/update_security_release.js +++ b/lib/update_security_release.js @@ -1,31 +1,23 @@ import { - NEXT_SECURITY_RELEASE_FOLDER, - NEXT_SECURITY_RELEASE_REPOSITORY, checkoutOnSecurityReleaseBranch, checkRemote, commitAndPushVulnerabilitiesJSON, validateDate, pickReport, getReportSeverity, - getSummary + getSummary, + SecurityRelease } from './security-release/security-release.js'; import fs from 'node:fs'; -import path from 'node:path'; import auth from './auth.js'; import Request from './request.js'; import nv from '@pkgjs/nv'; -export default class UpdateSecurityRelease { - repository = NEXT_SECURITY_RELEASE_REPOSITORY; - constructor(cli) { - this.cli = cli; - } - +export default class UpdateSecurityRelease extends SecurityRelease { async sync() { checkRemote(this.cli, this.repository); - const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); - const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath); + const content = this.readVulnerabilitiesJSON(); const credentials = await auth({ github: true, h1: true @@ -52,6 +44,7 @@ export default class UpdateSecurityRelease { prURL }; } + const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2)); this.cli.ok('Synced vulnerabilities.json with HackerOne'); } @@ -78,22 +71,6 @@ export default class UpdateSecurityRelease { cli.ok('Done!'); } - readVulnerabilitiesJSON(vulnerabilitiesJSONPath) { - const exists = fs.existsSync(vulnerabilitiesJSONPath); - - if (!exists) { - this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`); - process.exit(1); - } - - return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8')); - } - - getVulnerabilitiesJSONPath() { - return path.join(process.cwd(), - NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json'); - } - async updateJSONReleaseDate(releaseDate) { const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath(); const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath); @@ -163,7 +140,7 @@ export default class UpdateSecurityRelease { const programId = await this.getNodeProgramId(req); const cves = await this.promptCVECreation(req, reports, programId); this.assignCVEtoReport(cves, reports); - this.updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath); + this.updateVulnerabilitiesJSON(content); this.updateHackonerReportCve(req, reports); } @@ -195,18 +172,6 @@ export default class UpdateSecurityRelease { } } - updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath) { - this.cli.startSpinner(`Updating vulnerabilities.json from\ - ${vulnerabilitiesJSONPath}..`); - const filePath = path.resolve(vulnerabilitiesJSONPath); - fs.writeFileSync(filePath, JSON.stringify(content, null, 2)); - // push the changes to the repository - commitAndPushVulnerabilitiesJSON(filePath, - 'chore: updated vulnerabilities.json with CVEs', - { cli: this.cli, repository: this.repository }); - this.cli.stopSpinner(`Done updating vulnerabilities.json from ${filePath}`); - } - async promptCVECreation(req, reports, programId) { const supportedVersions = (await nv('supported')); const cves = []; diff --git a/package.json b/package.json index a0fd2135..1f1d6912 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-core/utils", - "version": "5.4.0", + "version": "5.5.0", "description": "Utilities for Node.js core collaborators", "type": "module", "engines": {