diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index d0fb4023..65959c9b 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x, 19.x] os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index d1954626..d0230111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [3.0.0](https://github.com/nodejs/node-core-utils/compare/v2.1.3...v3.0.0) (2023-03-08) + + +### ⚠ BREAKING CHANGES + +* Node.js 14.x is no longer supported. + +### Features + +* ignore .md files when do `requiresJenkinsRun` check ([#641](https://github.com/nodejs/node-core-utils/issues/641)) ([62f266f](https://github.com/nodejs/node-core-utils/commit/62f266fa685d849ef43943ed0e816fcab4b8affe)) + + +### Bug Fixes + +* do not run `git cherry-pick --abort` on failure ([#671](https://github.com/nodejs/node-core-utils/issues/671)) ([1e6f5d3](https://github.com/nodejs/node-core-utils/commit/1e6f5d3cbc7e837f1590122458b15f24ff9b378d)) +* switch to undici for requests to fix stream close errors ([#666](https://github.com/nodejs/node-core-utils/issues/666)) ([f759e7a](https://github.com/nodejs/node-core-utils/commit/f759e7a9495bb079abcbbcc5c2df4f311e67b779)) +* treat `fast-track` with not enough approvals as non-fatal ([#676](https://github.com/nodejs/node-core-utils/issues/676)) ([b324c99](https://github.com/nodejs/node-core-utils/commit/b324c99bca3a77be9076a208598b34196cf9413b)) +* use correct V8 tag for major updates ([#675](https://github.com/nodejs/node-core-utils/issues/675)) ([ebcf18e](https://github.com/nodejs/node-core-utils/commit/ebcf18e402d5a3e647c7eae6fb799f3a049ee244)) +* **wpt:** remove stale fixtures before pulling fresh ones ([#679](https://github.com/nodejs/node-core-utils/issues/679)) ([b78efc5](https://github.com/nodejs/node-core-utils/commit/b78efc5199856d11157de45ccb06d2ce34cb2b56)) + ## [2.1.3](https://github.com/nodejs/node-core-utils/compare/v2.1.2...v2.1.3) (2022-12-06) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60170ce3..a20b6328 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ Writing good commit logs is important. A commit log should describe what changed and why. Follow these guidelines when writing one: 1. The first line should be a short description of the change - (e.g. "get-metadata: check if the committer matches the author"). + (e.g. "get-metadata: check if the committer matches the author"). 2. Keep the second line blank. 3. Wrap all lines at 72 columns. diff --git a/README.md b/README.md index 60e0a361..8d378ceb 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ npm link Most of the tools need your GitHub credentials to work. You can either 1. Run any of the tools and you will be asked in a prompt to provide your - username and password in order to create a personal access token. + username and password in order to create a personal access token. 2. Or, create a personal access token yourself on GitHub, then set them up - using an editor. + using an editor. If you prefer option 2, [follow these instructions](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) @@ -93,22 +93,22 @@ these commands. To obtain the Jenkins API token 1. Open -`https://ci.nodejs.org/user//configure` (replace -\ with your own GitHub ID) + `https://ci.nodejs.org/user//configure` (replace + \ with your own GitHub username). 2. Click on the `ADD NEW TOKEN` button in the `API Token` section. 3. Enter an identifiable name (for example, `node-core-utils`) for this - token in the inbox that appears, and click `GENERATE`. + token in the inbox that appears, and click `GENERATE`. 4. Copy the generated token. 5. Add it into your `ncurc` file (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`) - with `jenkins_token` as key, like this: + with `jenkins_token` as key, like this: - ```json - { - "username": "your_github_username", - "token": "your_github_token", - "jenkins_token": "your_jenkins_token" - } - ``` + ```json + { + "username": "your_github_username", + "token": "your_github_token", + "jenkins_token": "your_jenkins_token" + } + ``` ### Make sure your credentials won't be committed @@ -136,8 +136,8 @@ If you encounter an error that you cannot fix by yourself, please 1. Make sure you update NCU to the latest version 2. Try again with the `NCU_VERBOSITY=debug` environment variable set and - open an issue at https://github.com/nodejs/node-core-utils/issues with - detailed logs. + open an issue at https://github.com/nodejs/node-core-utils/issues with + detailed logs. ## Contributing diff --git a/docs/ncu-ci.md b/docs/ncu-ci.md index 967a862f..ecea67b9 100644 --- a/docs/ncu-ci.md +++ b/docs/ncu-ci.md @@ -106,24 +106,24 @@ Unstable https://ci.nodejs.org/job/node-test-commit-arm-fanned/15212/ Possible use cases: 1. Walk CI for the latest 100 runs of `node-test-pull-request`, -aggregate failures, write the results into a Markdown file, -and then cache the responses from Jenkins so that the next time the command -is run, it picks up cached data written on disk for jobs whose results -are known. + aggregate failures, write the results into a Markdown file, + and then cache the responses from Jenkins so that the next time the command + is run, it picks up cached data written on disk for jobs whose results + are known. -Note: results are cached in `${ncu_installation_path}/.ncu/cache`, so you -may want to clean it up from time to time. + Note: results are cached in `${ncu_installation_path}/.ncu/cache`, so you + may want to clean it up from time to time. -``` -ncu-ci walk pr --stats --cache --markdown results.md -``` + ``` + ncu-ci walk pr --stats --cache --markdown results.md + ``` 2. Walk CI for the latest 100 runs of `node-test-pull-request`, and then write the -failures into a JSON file named database.json. + failures into a JSON file named database.json. -``` -ncu-ci walk pr --json database.json -``` + ``` + ncu-ci walk pr --json database.json + ``` ### `ncu-ci pr ` diff --git a/lib/auth.js b/lib/auth.js index 481d5b8f..c13ef196 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,13 +1,10 @@ import fs from 'node:fs'; import { ClientRequest } from 'node:http'; -import util from 'node:util'; -import ghauthBase from 'ghauth'; +import ghauth from 'ghauth'; import { getMergedConfig, getNcurcPath } from './config.js'; -const ghauth = util.promisify(ghauthBase); - export default lazy(auth); function errorExit(message) { @@ -50,7 +47,8 @@ async function tryCreateGitHubToken(githubAuth) { credentials = await githubAuth({ noSave: true, scopes: ['user:email', 'read:org'], - note: 'node-core-utils CLI tools' + note: 'node-core-utils CLI tools', + noDeviceFlow: true }); } catch (e) { errorExit(`Could not get token: ${e.message}`); diff --git a/lib/ci/run_ci.js b/lib/ci/run_ci.js index 74317908..46891c21 100644 --- a/lib/ci/run_ci.js +++ b/lib/ci/run_ci.js @@ -1,4 +1,4 @@ -import FormData from 'form-data'; +import { FormData } from 'undici'; import { CI_DOMAIN, diff --git a/lib/file.js b/lib/file.js index 7df66966..49448b0d 100644 --- a/lib/file.js +++ b/lib/file.js @@ -37,3 +37,7 @@ export function readJson(file) { } return {}; }; + +export function removeDirectory(directory) { + return fs.promises.rm(directory, { recursive: true, force: true }); +} diff --git a/lib/landing_session.js b/lib/landing_session.js index 3d7f8633..c4745be1 100644 --- a/lib/landing_session.js +++ b/lib/landing_session.js @@ -121,8 +121,6 @@ export default class LandingSession extends Session { ignoreFailure: false }); } catch (ex) { - await forceRunAsync('git', ['cherry-pick', '--abort']); - cli.error('Failed to apply patches'); process.exit(1); } @@ -436,6 +434,10 @@ export default class LandingSession extends Session { if (!forceLand) { cli.info('Please fix the commit message and try again.'); + cli.log('Please manually ammend the commit message, by running\n' + + '`git commit --amend`\n' + + 'Once commit message is fixed, finish the landing command running\n' + + '`git node land --continue`'); process.exit(1); } } diff --git a/lib/links.js b/lib/links.js index 6a3e534f..b14752a0 100644 --- a/lib/links.js +++ b/lib/links.js @@ -1,4 +1,4 @@ -import cheerio from 'cheerio'; +import * as cheerio from 'cheerio'; const FIXES_RE = /(Close[ds]?|Fix(e[ds])?|Resolve[sd]?)\s*:\s*(\S+)/mgi; const FIX_RE = /(Close[ds]?|Fix(e[ds])?|Resolve[sd]?)\s*:\s*(\S+)/i; diff --git a/lib/pr_checker.js b/lib/pr_checker.js index 10968da5..e48bf2d5 100644 --- a/lib/pr_checker.js +++ b/lib/pr_checker.js @@ -27,6 +27,7 @@ const WAIT_TIME_SINGLE_APPROVAL = 24 * 7; const GITHUB_SUCCESS_CONCLUSIONS = ['SUCCESS', 'NEUTRAL', 'SKIPPED']; const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/; +const FAST_TRACK_MIN_APPROVALS = 2; const GIT_CONFIG_GUIDE_URL = 'https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork'; export default class PRChecker { @@ -138,7 +139,7 @@ export default class PRChecker { const { requestedChanges, approved } = reviewers; const labels = pr.labels.nodes.map((l) => l.name); - const isFastTracked = labels.includes('fast-track'); + let isFastTracked = labels.includes('fast-track'); const isCodeAndLearn = labels.includes('code-and-learn'); const isSemverMajor = labels.includes('semver-major'); @@ -168,6 +169,7 @@ export default class PRChecker { } } + let fastTrackAppendix = ''; if (isFastTracked) { const comment = [...this.comments].reverse().find((c) => FAST_TRACK_RE.test(c.bodyText)); @@ -183,14 +185,15 @@ export default class PRChecker { r.user.login !== pr.author.login && collaborators.includes(r.user.login.toLowerCase())).length; - if (requester === pr.author.login && approvals < 2) { - cli.error('The fast-track request requires' + - " at least two collaborators' approvals (👍)."); - return false; - } else if (approvals === 0) { - cli.error('The fast-track request requires' + - " at least one collaborator's approval (👍)."); - return false; + const missingFastTrackApprovals = FAST_TRACK_MIN_APPROVALS - approvals - + (requester === pr.author.login ? 0 : 1); + if (missingFastTrackApprovals > 0) { + isFastTracked = false; + fastTrackAppendix = ' (or 0 hours if there ' + + `${missingFastTrackApprovals === 1 ? 'is' : 'are'} ` + + `${missingFastTrackApprovals} more approval` + + `${missingFastTrackApprovals === 1 ? '' : 's'} (👍) of ` + + 'the fast-track request from collaborators).'; } } @@ -211,10 +214,12 @@ export default class PRChecker { if (timeLeftMulti === 0) { const timeLeftMins = this.waitTimeMultiApproval * 60 - minutesFromCreateTime; - cli.error(`This PR needs to wait ${timeLeftMins} more minutes to land`); + cli.error(`This PR needs to wait ${timeLeftMins} ` + + `more minutes to land${fastTrackAppendix}`); return false; } - cli.error(`This PR needs to wait ${timeLeftMulti} more hours to land`); + cli.error(`This PR needs to wait ${timeLeftMulti} more ` + + `hours to land${fastTrackAppendix}`); return false; } @@ -224,7 +229,8 @@ export default class PRChecker { } timeLeftMulti = timeLeftMulti < 0 || isFastTracked ? 0 : timeLeftMulti; cli.error(`This PR needs to wait ${timeLeftSingle} more hours to land ` + - `(or ${timeLeftMulti} hours if there is one more approval)`); + `(or ${timeLeftMulti} hours if there is one more approval)` + + fastTrackAppendix); return false; } } @@ -404,6 +410,13 @@ export default class PRChecker { return false; } + const files = pr.files.nodes; + + // Don't require Jenkins run for doc-only change. + if (files.every(({ path }) => path.endsWith('.md'))) { + return false; + } + const ciNeededFolderRx = /^(deps|lib|src|test)\//; const ciNeededToolFolderRx = /^tools\/(code_cache|gyp|icu|inspector|msvs|snapshot|v8_gypfiles)/; @@ -416,7 +429,7 @@ export default class PRChecker { ]; const ciNeededExtensionList = ['.gyp', '.gypi', '.bat']; - return pr.files.nodes.some( + return files.some( ({ path }) => ciNeededFolderRx.test(path) || ciNeededToolFolderRx.test(path) || diff --git a/lib/proxy.js b/lib/proxy.js index f01cdc43..b9d30a89 100644 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -1,7 +1,7 @@ import { globalAgent } from 'node:https'; import { spawnSync } from 'child_process'; -import ProxyAgent from 'proxy-agent'; +import { ProxyAgent } from 'undici'; import { getMergedConfig } from './config.js'; diff --git a/lib/request.js b/lib/request.js index 6002da6f..e15d3904 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,6 +1,6 @@ import fs from 'node:fs'; -import fetch from 'node-fetch'; +import { fetch } from 'undici'; import { CI_DOMAIN } from './ci/ci_type_parser.js'; import proxy from './proxy.js'; diff --git a/lib/session.js b/lib/session.js index 18256c56..9b120f39 100644 --- a/lib/session.js +++ b/lib/session.js @@ -1,8 +1,6 @@ import path from 'node:path'; import fs from 'node:fs'; -import rimraf from 'rimraf'; - import { getMergedConfig, getNcuDir } from './config.js'; import { readJson, writeJson, readFile, writeFile } from './file.js'; import { @@ -148,13 +146,13 @@ export default class Session { try { sess = this.session; } catch (err) { - return rimraf.sync(this.sessionPath); + return fs.rmSync(this.sessionPath, { recursive: true, force: true }); } if (sess.prid && sess.prid === this.prid) { - rimraf.sync(this.pullDir); + fs.rmSync(this.pullDir, { recursive: true, force: true }); } - rimraf.sync(this.sessionPath); + fs.rmSync(this.sessionPath, { recursive: true, force: true }); } get statusPath() { diff --git a/lib/update-v8/majorUpdate.js b/lib/update-v8/majorUpdate.js index 10d74826..736c4344 100644 --- a/lib/update-v8/majorUpdate.js +++ b/lib/update-v8/majorUpdate.js @@ -54,7 +54,7 @@ function checkoutBranch() { '--sort', 'version:refname' ); - const tags = res.stdout.split('\n'); + const tags = res.stdout.split('\n').filter(tag => versionReg.test(tag)); const lastTag = tags[tags.length - 1]; if (lastTag) version = lastTag; if (version.split('.').length === 3) { diff --git a/lib/wpt/index.js b/lib/wpt/index.js index 52d2376b..601b207b 100644 --- a/lib/wpt/index.js +++ b/lib/wpt/index.js @@ -3,7 +3,9 @@ import path from 'node:path'; import _ from 'lodash'; import GitHubTree from '../github/tree.js'; -import { writeFile, readJson, writeJson, readFile } from '../file.js'; +import { + writeFile, readJson, writeJson, readFile, removeDirectory +} from '../file.js'; import { shortSha } from '../utils.js'; @@ -73,6 +75,9 @@ export class WPTUpdater { assets = await this.getAssetList(); } + this.cli.startSpinner('Removing stale assets...'); + await removeDirectory(this.fixtures(this.path)); + this.cli.startSpinner('Pulling assets...'); await Promise.all(assets.map( (asset) => this.pullTextFile(fixtures, asset.name) diff --git a/package.json b/package.json index 63728c34..67e0fa6f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "node-core-utils", - "version": "2.1.3", + "version": "3.0.0", "description": "Utilities for Node.js core collaborators", "type": "module", + "engines": { + "node": ">=16.8.0" + }, "bin": { "get-metadata": "./bin/get-metadata.js", "git-node": "./bin/git-node.js", @@ -31,38 +34,34 @@ ], "license": "MIT", "dependencies": { - "branch-diff": "^1.10.5", - "chalk": "^5.0.1", - "changelog-maker": "^2.8.0", - "cheerio": "^1.0.0-rc.10", + "branch-diff": "^2.1.0", + "chalk": "^5.2.0", + "changelog-maker": "^3.2.1", + "cheerio": "^1.0.0-rc.12", "clipboardy": "^3.0.0", - "core-validate-commit": "^3.16.0", + "core-validate-commit": "^3.18.0", "enquirer": "^2.3.6", - "execa": "^6.1.0", - "figures": "^4.0.1", - "form-data": "^4.0.0", - "ghauth": "^4.0.0", - "inquirer": "^8.2.4", - "listr2": "^4.0.5", + "execa": "^7.0.0", + "figures": "^5.0.0", + "ghauth": "^5.0.1", + "inquirer": "^9.1.4", + "listr2": "^5.0.7", "lodash": "^4.17.21", "log-symbols": "^5.1.0", - "node-fetch": "^3.2.4", - "ora": "^6.1.0", - "proxy-agent": "^5.0.0", - "replace-in-file": "^6.3.2", - "rimraf": "^3.0.2", - "which": "^2.0.2", - "yargs": "^17.5.0" + "ora": "^6.1.2", + "replace-in-file": "^6.3.5", + "undici": "^5.20.0", + "which": "^3.0.0", + "yargs": "^17.7.1" }, "devDependencies": { - "c8": "^7.11.2", - "eslint": "^8.15.0", + "c8": "^7.13.0", + "eslint": "^8.35.0", "eslint-config-standard": "^17.0.0", - "eslint-plugin-import": "^2.26.0", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-standard": "^4.1.0", - "mocha": "^10.0.0", - "sinon": "^14.0.0" + "eslint-plugin-promise": "^6.1.1", + "mocha": "^10.2.0", + "sinon": "^15.0.1" } } diff --git a/test/common.js b/test/common.js index 464883b8..bf07a2b1 100644 --- a/test/common.js +++ b/test/common.js @@ -2,8 +2,6 @@ import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; -import rimraf from 'rimraf'; - const tmpdirPath = fileURLToPath(new URL('tmp', import.meta.url)); export const tmpdir = { @@ -11,7 +9,7 @@ export const tmpdir = { return tmpdirPath; }, refresh() { - rimraf.sync(this.path); + fs.rmSync(this.path, { recursive: true, force: true }); fs.mkdirSync(this.path, { recursive: true }); } }; diff --git a/test/fixtures/pull_requests/code-change.json b/test/fixtures/pull_requests/code-change.json new file mode 100644 index 00000000..d668419c --- /dev/null +++ b/test/fixtures/pull_requests/code-change.json @@ -0,0 +1,23 @@ +{ + "createdAt": "2022-07-12T11:55:42Z", + "authorAssociation": "MEMBER", + "author": { + "login": "F3n67u", + "email": "F3n67u@outlook.com", + "name": "Feng Yu" + }, + "url": "https://github.com/nodejs/node/pull/43796", + "bodyHTML": "\n

Fix #43795

", + "bodyText": "Fix #43795", + "labels": { "nodes": [{ "name": "http" }] }, + "files": { "nodes": [{ "path": "lib/_http_server.js" }] }, + "title": "http: check if `socket` is null before destroy", + "baseRefName": "main", + "headRefName": "closeIdleConnections", + "changedFiles": 1, + "mergeable": "MERGEABLE", + "closed": false, + "closedAt": null, + "merged": false, + "mergedAt": null +} diff --git a/test/fixtures/pull_requests/doc-only-in-tools.json b/test/fixtures/pull_requests/doc-only-in-tools.json new file mode 100644 index 00000000..6904e705 --- /dev/null +++ b/test/fixtures/pull_requests/doc-only-in-tools.json @@ -0,0 +1,31 @@ +{ + "createdAt": "2022-06-19T03:31:58Z", + "authorAssociation": "MEMBER", + "author": { + "login": "F3n67u", + "email": "F3n67u@outlook.com", + "name": "Feng Yu" + }, + "url": "https://github.com/nodejs/node/pull/43483", + "bodyHTML": "\n

unicode-org/icu repo has renamed its default branch to main also, this pr update the link to unicode-org/icu old master branch.

", + "bodyText": "unicode-org/icu repo has renamed its default branch to main also, this pr update the link to unicode-org/icu old master branch.", + "labels": { + "nodes": [ + { "name": "tools" }, + { "name": "i18n-api" }, + { "name": "fast-track" }, + { "name": "author ready" }, + { "name": "icu" } + ] + }, + "files": { "nodes": [{ "path": "tools/icu/README.md" }] }, + "title": "tools: update link of `ICU data slicer`", + "baseRefName": "main", + "headRefName": "doc/icu", + "changedFiles": 1, + "mergeable": "UNKNOWN", + "closed": true, + "closedAt": "2022-06-20T09:20:58Z", + "merged": true, + "mergedAt": "2022-06-20T09:20:58Z" +} diff --git a/test/fixtures/run-auth-github.js b/test/fixtures/run-auth-github.js index 25cd4bb2..d7151ea6 100644 --- a/test/fixtures/run-auth-github.js +++ b/test/fixtures/run-auth-github.js @@ -4,7 +4,8 @@ async function mockCredentials(options) { assert.deepStrictEqual(options, { noSave: true, scopes: ['user:email', 'read:org'], - note: 'node-core-utils CLI tools' + note: 'node-core-utils CLI tools', + noDeviceFlow: true }); return { user: 'nyancat', diff --git a/test/unit/auth.test.js b/test/unit/auth.test.js index d933d4ca..6e13b9dc 100644 --- a/test/unit/auth.test.js +++ b/test/unit/auth.test.js @@ -4,8 +4,6 @@ import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import assert from 'node:assert'; -import rimraf from 'rimraf'; - let testCounter = 0; // for tmp directories const FIRST_TIME_MSG = @@ -136,7 +134,7 @@ function runAuthScript( if (ncurc[envVar] === undefined) continue; newEnv[envVar] = fileURLToPath(new URL(`tmp-${testCounter++}`, import.meta.url)); - rimraf.sync(newEnv[envVar]); + fs.rmSync(newEnv[envVar], { recursive: true, force: true }); fs.mkdirSync(newEnv[envVar], { recursive: true }); const ncurcPath = path.resolve(newEnv[envVar], @@ -169,8 +167,12 @@ function runAuthScript( try { assert.strictEqual(stderr, error); assert.strictEqual(expect.length, 0); - if (newEnv.HOME) rimraf.sync(newEnv.HOME); - if (newEnv.XDG_CONFIG_HOME) rimraf.sync(newEnv.XDG_CONFIG_HOME); + if (newEnv.HOME) { + fs.rmSync(newEnv.HOME, { recursive: true, force: true }); + } + if (newEnv.XDG_CONFIG_HOME) { + fs.rmSync(newEnv.XDG_CONFIG_HOME, { recursive: true, force: true }); + } } catch (err) { reject(err); } diff --git a/test/unit/ci_start.test.js b/test/unit/ci_start.test.js index 2280131e..65154f5a 100644 --- a/test/unit/ci_start.test.js +++ b/test/unit/ci_start.test.js @@ -1,7 +1,7 @@ import assert from 'assert'; import sinon from 'sinon'; -import FormData from 'form-data'; +import { FormData } from 'undici'; import { RunPRJob, diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index 86ff437c..441200e5 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -644,8 +644,9 @@ describe('PRChecker', () => { [['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'], ['This PR is being fast-tracked']], error: - [['The fast-track request requires at' + - " least two collaborators' approvals (👍)."]] + [['This PR needs to wait 24 more hours to land (or 0 hours if ' + + 'there are 2 more approvals (👍) of the fast-track request from ' + + 'collaborators).']] }; const pr = Object.assign({}, firstTimerPR, { @@ -694,8 +695,9 @@ describe('PRChecker', () => { [['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'], ['This PR is being fast-tracked']], error: - [['The fast-track request requires at' + - " least one collaborator's approval (👍)."]] + [['This PR needs to wait 24 more hours to land (or 0 hours if ' + + 'there is 1 more approval (👍) of the fast-track request from ' + + 'collaborators).']] }; const pr = Object.assign({}, firstTimerPR, { @@ -727,6 +729,51 @@ describe('PRChecker', () => { cli.assertCalledWith(expectedLogs); }); + it('should error when not enough approvals or fast-track approvals', () => { + const cli = new TestCLI(); + + const expectedLogs = { + ok: + [['Approvals: 1'], + ['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624']], + info: + [['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'], + ['This PR is being fast-tracked']], + error: + [['This PR needs to wait 144 more hours to land (or 24 hours if ' + + 'there is one more approval) (or 0 hours if there is 1 more ' + + 'approval (👍) of the fast-track request from collaborators).']] + }; + + const pr = Object.assign({}, firstTimerPR, { + createdAt: LT_48H, + labels: { + nodes: [ + { name: 'fast-track' } + ] + } + }); + + const data = { + pr, + reviewers: singleGreenReviewer, + comments: commentsWithFastTrackInsuffientApprovals, + reviews: approvingReviews, + commits: [], + collaborators, + authorIsNew: () => true, + getThread() { + return PRData.prototype.getThread.call(this); + } + }; + const checker = new PRChecker(cli, data, {}, argv); + + cli.clearCalls(); + const status = checker.checkReviewsAndWait(new Date(NOW)); + assert(!status); + cli.assertCalledWith(expectedLogs); + }); + it('should error when missing fast-track request comment', () => { const cli = new TestCLI(); @@ -971,6 +1018,92 @@ describe('PRChecker', () => { }); }); + it('should succeed if doc-only changes in tools dir without Jenkins', + async() => { + const cli = new TestCLI(); + + const expectedLogs = { + ok: [ + ['Last GitHub CI successful'] + ], + info: [ + ['Green GitHub CI is sufficient'] + ] + }; + + const data = { + pr: pullRequests['doc-only-in-tools'], + reviewers: allGreenReviewers, + comments: [], + reviews: approvingReviews, + commits: githubCI['check-suite-success'], + collaborators, + authorIsNew: () => true, + getThread() { + return PRData.prototype.getThread.call(this); + } + }; + const checker = new PRChecker( + cli, + data, + { + json: sinon.stub().callsFake(await function() { + return undefined; + }) + }, + argv); + + cli.clearCalls(); + const status = await checker.checkCI(); + assert(status); + cli.assertCalledWith(expectedLogs, { + ignore: ['startSpinner', 'updateSpinner', 'stopSpinner'] + }); + } + ); + + it('should fail if code changes without Jenkins', async() => { + const cli = new TestCLI(); + + const expectedLogs = { + error: [ + ['No Jenkins CI runs detected'] + ], + ok: [ + ['Last GitHub CI successful'] + ] + }; + + const data = { + pr: pullRequests['code-change'], + reviewers: allGreenReviewers, + comments: [], + reviews: approvingReviews, + commits: githubCI['check-suite-success'], + collaborators, + authorIsNew: () => true, + getThread() { + return PRData.prototype.getThread.call(this); + } + }; + const checker = new PRChecker( + cli, + data, + { + json: sinon.stub().callsFake(await function() { + return undefined; + }) + }, + argv); + + cli.clearCalls(); + const status = await checker.checkCI(); + assert(!status); + cli.assertCalledWith(expectedLogs, { + ignore: ['startSpinner', 'updateSpinner', 'stopSpinner'] + }); + }); + it('should succeed if doc-only changes with failed Jenkins', async() => { const cli = new TestCLI();