From 774aa6c849ea4671d413fa04c1ce78256ea3e61a Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:50:31 -0700 Subject: [PATCH 1/7] chore: replace cheerio with htmlparser2 (#1176) Assisted-by: codex:gpt-5.6-sol --- lib/links.js | 42 +++-- package-lock.json | 379 +++++++++------------------------------- package.json | 2 +- test/unit/links.test.js | 70 +++++++- 4 files changed, 184 insertions(+), 309 deletions(-) diff --git a/lib/links.js b/lib/links.js index 412a9e7b..2e8a391b 100644 --- a/lib/links.js +++ b/lib/links.js @@ -1,4 +1,4 @@ -import * as cheerio from 'cheerio'; +import { Parser } from 'htmlparser2'; 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; @@ -13,7 +13,26 @@ export class LinkParser { constructor(owner, repo, html) { this.owner = owner; this.repo = repo; - this.$ = cheerio.load(html); + this.text = ''; + this.links = []; + + let currentLink; + const parser = new Parser({ + onopentag: (name, attributes) => { + if (name === 'a') { + currentLink = { href: attributes.href, text: '' }; + this.links.push(currentLink); + } + }, + ontext: (text) => { + this.text += text; + if (currentLink) currentLink.text += text; + }, + onclosetag: (name) => { + if (name === 'a') currentLink = undefined; + } + }); + parser.end(html); } getFixesUrlsFromArray(arr) { @@ -55,32 +74,25 @@ export class LinkParser { // Do this so we can reliably get the correct url. // Otherwise, the number could reference a PR or an issue. getUrlFromOP(ref) { - const as = this.$('a'); - const links = as.map((i, el) => this.$(el)).get(); - for (const link of links) { - const text = link.text(); - if (text === ref) { - const href = link.attr('href'); - if (href) return href; + for (const link of this.links) { + if (link.text === ref) { + if (link.href) return link.href; } } } getFixes() { - const text = this.$.text(); - const fixes = text.match(FIXES_RE) || []; + const fixes = this.text.match(FIXES_RE) || []; return this.getFixesUrlsFromArray(fixes); } getRefs() { - const text = this.$.text(); - const refs = text.match(REFS_RE) || []; + const refs = this.text.match(REFS_RE) || []; return this.getRefsUrlsFromArray(refs); } getAltPrUrl() { - const text = this.$.text(); - const refs = text.match(PR_RE) || []; + const refs = this.text.match(PR_RE) || []; return this.getPRUrlsFromArray(refs); } } diff --git a/package-lock.json b/package-lock.json index 4e6c51bb..1bc8b800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "@pkgjs/nv", "branch-diff", "changelog-maker", - "cheerio", "core-validate-commit", "ghauth", "git-secure-tag", + "htmlparser2", "js-yaml", "listr2", "ora", @@ -34,10 +34,10 @@ "@pkgjs/nv": "^0.3.0", "branch-diff": "^3.1.1", "changelog-maker": "^4.4.1", - "cheerio": "^1.0.0", "core-validate-commit": "^6.0.0", "ghauth": "^7.0.1", "git-secure-tag": "^2.3.1", + "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "listr2": "^11.1.0", "ora": "^9.0.0", @@ -1936,13 +1936,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "inBundle": true, - "license": "ISC" - }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -2249,60 +2242,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/cheerio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.1.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.19.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cheerio/node_modules/undici": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.26.0.tgz", - "integrity": "sha512-3O9Tf67pGhgOv9jM35AbhkXAKi13f3oy3aE4CSgr+TckGeY+/iu97ZXN+J7DpHPzLbVApFd1IFhcnBjREYXYcg==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -2551,36 +2490,6 @@ "node": ">= 8" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2806,65 +2715,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2887,20 +2737,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2925,19 +2761,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -4424,9 +4247,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -4437,20 +4260,94 @@ "inBundle": true, "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "inBundle": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -4477,19 +4374,6 @@ "node": ">=10.19.0" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6703,19 +6587,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -7070,59 +6941,6 @@ "node": ">=0.10.0" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9833,29 +9651,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/which-boxed-primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", diff --git a/package.json b/package.json index 3619505d..99d4ef00 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,10 @@ "@pkgjs/nv": "^0.3.0", "branch-diff": "^3.1.1", "changelog-maker": "^4.4.1", - "cheerio": "^1.0.0", "core-validate-commit": "^6.0.0", "ghauth": "^7.0.1", "git-secure-tag": "^2.3.1", + "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "listr2": "^11.1.0", "ora": "^9.0.0", diff --git a/test/unit/links.test.js b/test/unit/links.test.js index d3b064b6..b3c06742 100644 --- a/test/unit/links.test.js +++ b/test/unit/links.test.js @@ -1,7 +1,13 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { LinkParser, parsePRFromURL } from '../../lib/links.js'; +import { + getMachineUrl, + getPrURL, + LinkParser, + parsePrURL, + parsePRFromURL +} from '../../lib/links.js'; import * as fixtures from '../fixtures/index.js'; @@ -46,6 +52,38 @@ describe('LinkParser', () => { } }); + it('should parse an alternate PR URL', () => { + const url = 'https://github.com/nodejs/node/pull/12345'; + const parser = new LinkParser( + 'nodejs', + 'node', + `PR-URL: ${url}` + ); + + assert.deepStrictEqual(parser.getAltPrUrl(), [url]); + }); + + it('should ignore references without matching links', () => { + const parser = new LinkParser( + 'nodejs', + 'node', + 'Fixes: #1\nRefs: #2\nPR-URL: https://example.com/pull/3' + ); + + assert.deepStrictEqual(parser.getFixes(), []); + assert.deepStrictEqual(parser.getRefs(), []); + assert.deepStrictEqual(parser.getAltPrUrl(), []); + }); + + it('should ignore malformed array entries', () => { + const parser = new LinkParser('nodejs', 'node', ''); + + assert.deepStrictEqual(parser.getAltPrUrl(), []); + assert.deepStrictEqual(parser.getFixesUrlsFromArray(['invalid']), []); + assert.deepStrictEqual(parser.getRefsUrlsFromArray(['invalid']), []); + assert.deepStrictEqual(parser.getPRUrlsFromArray(['invalid']), []); + }); + it('should parse PR URL', () => { const tests = [{ input: 'https://github.com/nodejs/node/pull/15148', @@ -92,3 +130,33 @@ describe('LinkParser', () => { } }); }); + +describe('link formatting', () => { + it('should format a pull request URL', () => { + assert.strictEqual( + getPrURL({ owner: 'nodejs', repo: 'node', prid: 12345 }), + 'https://github.com/nodejs/node/pull/12345' + ); + }); + + it('should format a machine link', () => { + assert.strictEqual( + getMachineUrl({ hostname: 'test-host', url: 'https://ci.example.test' }), + '[test-host](https://ci.example.test)' + ); + }); +}); + +describe('parsePrURL', () => { + it('should parse a PR-URL trailer', () => { + assert.deepStrictEqual( + parsePrURL('PR-URL: https://github.com/nodejs/node/pull/12345'), + { owner: 'nodejs', repo: 'node', prid: 12345 } + ); + }); + + it('should return undefined for invalid input', () => { + assert.strictEqual(parsePrURL(12345), undefined); + assert.strictEqual(parsePrURL('not a PR-URL trailer'), undefined); + }); +}); From 377574cb51100b053bc8b5f4a93af0ed52c830de Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:50:47 -0700 Subject: [PATCH 2/7] chore: replace ora with yocto-spinner (#1186) --- lib/cli.js | 9 ++-- package-lock.json | 104 +++++++++------------------------------------- package.json | 4 +- 3 files changed, 25 insertions(+), 92 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index e0e71276..af849b73 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,5 +1,5 @@ -import ora from 'ora'; import { styleText } from 'node:util'; +import yoctoSpinner from 'yocto-spinner'; import * as inquirer from '@inquirer/prompts'; import { warning, error, info, success } from './figures.js'; @@ -29,7 +29,7 @@ export default class CLI { constructor(stream, options = {}) { const spinnerOptions = options.spinner ?? {}; this.stream = stream || process.stderr; - this.spinner = ora({ stream: this.stream, ...spinnerOptions }); + this.spinner = yoctoSpinner({ stream: this.stream, ...spinnerOptions }); this.SPINNER_STATUS = SPINNER_STATUS; this.QUESTION_TYPE = QUESTION_TYPE; this.figureIndent = ' '; @@ -154,10 +154,7 @@ export default class CLI { case INFO: symbol = info; } - const text = ' ' + rawText; - this.spinner.stopAndPersist({ - symbol, text - }); + this.spinner.stop(`${symbol} ${rawText}`); } write(text) { diff --git a/package-lock.json b/package-lock.json index 1bc8b800..51d6d95d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,11 @@ "htmlparser2", "js-yaml", "listr2", - "ora", "semver", "smol-toml", "undici", - "yargs" + "yargs", + "yocto-spinner" ], "license": "MIT", "dependencies": { @@ -40,11 +40,11 @@ "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "listr2": "^11.1.0", - "ora": "^9.0.0", "semver": "^7.7.1", "smol-toml": "^1.7.0", "undici": "^8.3.0", - "yargs": "^18.0.0" + "yargs": "^18.0.0", + "yocto-spinner": "^1.2.2" }, "bin": { "git-node": "bin/git-node.js", @@ -6748,73 +6748,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/cli-spinners": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", - "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "inBundle": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -8829,19 +8762,6 @@ "node": ">=8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -9941,6 +9861,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yocto-spinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.2.tgz", + "integrity": "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", diff --git a/package.json b/package.json index 99d4ef00..fefe8280 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,11 @@ "htmlparser2": "^12.0.0", "js-yaml": "^5.2.1", "listr2": "^11.1.0", - "ora": "^9.0.0", "semver": "^7.7.1", "smol-toml": "^1.7.0", "undici": "^8.3.0", - "yargs": "^18.0.0" + "yargs": "^18.0.0", + "yocto-spinner": "^1.2.2" }, "devDependencies": { "@eslint/js": "^9.39.4", From d4bbaeb3d291688e3ca4a1b577918ecb81848168 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:52:50 +0200 Subject: [PATCH 3/7] chore(deps-dev): bump flatted from 3.3.3 to 3.4.4 (#1190) Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51d6d95d..fb022103 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3537,9 +3537,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, From 98b8e3be26228ca845396bfc33eb21ed67196664 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 15 Sep 2026 17:28:03 +0200 Subject: [PATCH 4/7] fix: exclude stale matrix runs from CI reports (#1191) Signed-off-by: Filip Skokan --- lib/ci/build-types/normal_build.js | 2 +- lib/ci/build-types/test_run.js | 2 +- test/unit/ci_normal_build.test.js | 72 ++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/unit/ci_normal_build.test.js diff --git a/lib/ci/build-types/normal_build.js b/lib/ci/build-types/normal_build.js index 7c148b78..e7d541b4 100644 --- a/lib/ci/build-types/normal_build.js +++ b/lib/ci/build-types/normal_build.js @@ -76,7 +76,7 @@ export class NormalBuild extends Job { if (!upstream) { return true; } else { - return this.jobUrl.includes(upstream + ''); + return this.jobUrl.endsWith(`/${upstream}/`); } }); diff --git a/lib/ci/build-types/test_run.js b/lib/ci/build-types/test_run.js index 8d60dcf8..46f2364f 100644 --- a/lib/ci/build-types/test_run.js +++ b/lib/ci/build-types/test_run.js @@ -31,7 +31,7 @@ export class TestRun extends Job { ]; return this.failures; } - this.causes = this.getCause(data.actions) || {}; + this.cause = this.getCause(data.actions) || {}; this.builtOn = data.builtOn; } diff --git a/test/unit/ci_normal_build.test.js b/test/unit/ci_normal_build.test.js new file mode 100644 index 00000000..5c169eee --- /dev/null +++ b/test/unit/ci_normal_build.test.js @@ -0,0 +1,72 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { NormalBuild } from '../../lib/ci/build-types/normal_build.js'; +import TestCLI from '../fixtures/test_cli.js'; + +const jobName = 'node-test-commit-osx'; +const jobURL = `https://ci.nodejs.org/job/${jobName}/`; +const currentURL = `${jobURL}nodes=macos15-x64/73204/`; +const failureLog = 'not ok 1 parallel/test-example\n' + + ' ---\n severity: fail\n stack: |-\n AssertionError\n ...\n'; + +describe('Jenkins matrix failure reporting', () => { + for (const { name, upstreamBuild, reportRun } of [ + { name: 'ignores an older run', upstreamBuild: 73203, reportRun: false }, + { name: 'ignores a build-number prefix', upstreamBuild: 7320, reportRun: false }, + { name: 'ignores a build-number suffix', upstreamBuild: 3204, reportRun: false }, + { name: 'keeps runs without an upstream cause', reportRun: true } + ]) { + it(name, async(t) => { + const otherURL = `${jobURL}nodes=osx13-x64/${upstreamBuild ?? 73204}/`; + const responses = new Map([ + [`${jobURL}73204/api/json`, { + result: 'FAILURE', + runs: [ + { number: 73204, result: 'FAILURE', url: currentURL }, + { number: upstreamBuild ?? 73204, result: 'FAILURE', url: otherURL } + ] + }], + [`${currentURL}api/json`, { + builtOn: 'current-worker', + actions: [{}, { + causes: [{ upstreamBuild: 73204, upstreamProject: jobName }] + }] + }], + [`${otherURL}api/json`, { + builtOn: 'other-worker', + actions: upstreamBuild === undefined + ? [] + : [{}, { + causes: [{ upstreamBuild, upstreamProject: jobName }] + }] + }] + ]); + const request = { + async json(url) { + const key = url.split('?')[0]; + assert.ok(responses.has(key), `Unexpected JSON request: ${url}`); + return responses.get(key); + }, + text: t.mock.fn(async() => failureLog) + }; + const build = new NormalBuild(new TestCLI(), request, jobName, 73204); + const failures = await build.getResults(); + const expectedURLs = reportRun ? [currentURL, otherURL] : [currentURL]; + + assert.deepEqual(failures.map(failure => ({ + url: failure.url, + type: failure.type, + file: failure.file + })), expectedURLs.map(url => ({ + url: `${url}console`, + type: 'JS_TEST_FAILURE', + file: 'parallel/test-example' + }))); + assert.deepEqual( + request.text.mock.calls.map(call => call.arguments), + expectedURLs.map(url => [`${url}consoleText`]) + ); + }); + } +}); From 9f665faa719320adf57f5aae6934cc3e678a601a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 15 Sep 2026 17:28:09 +0200 Subject: [PATCH 5/7] fix: ignore fully skipped startup failures (#1170) --- lib/pr_checker.js | 79 ++++++++++++++++------------- lib/queries/PR.gql | 1 + test/unit/graphql_queries.test.js | 1 + test/unit/pr_checker.test.js | 82 +++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 34 deletions(-) diff --git a/lib/pr_checker.js b/lib/pr_checker.js index 5e41eb8b..b241a6cb 100644 --- a/lib/pr_checker.js +++ b/lib/pr_checker.js @@ -467,43 +467,54 @@ export default class PRChecker { } if (!GITHUB_SUCCESS_CONCLUSIONS.includes(conclusion)) { - hasFailures = true; + const runs = checkRuns?.nodes ?? []; + const allRunsSkipped = runs.length > 0 && + runs.length === checkRuns.totalCount && + runs.every((checkRun) => checkRun.status === 'COMPLETED' && + checkRun.conclusion === 'SKIPPED'); + + // GitHub can report a STARTUP_FAILURE for a workflow even though all + // of its conditional jobs were skipped. There is no failed check in + // that case, so the suite should not make the commit unlandable. + if (conclusion === 'STARTUP_FAILURE' && allRunsSkipped) { + continue; + } - // If we have detailed checkRuns, show specific failing jobs - if (checkRuns && checkRuns.nodes && checkRuns.nodes.length > 0) { - for (const checkRun of checkRuns.nodes) { - if (checkRun.status === 'COMPLETED' && - !GITHUB_SUCCESS_CONCLUSIONS.includes(checkRun.conclusion)) { - if (checkRun.conclusion === 'CANCELLED') { - cancelledJobs.push({ - name: checkRun.name, - conclusion: checkRun.conclusion, - url: checkRun.detailsUrl - }); - } else { - failedJobs.push({ - name: checkRun.name, - conclusion: checkRun.conclusion, - url: checkRun.detailsUrl - }); - } + hasFailures = true; + let reportedFailure = false; + + // If we have detailed checkRuns, show specific failing jobs. + for (const checkRun of runs) { + if (checkRun.status === 'COMPLETED' && + !GITHUB_SUCCESS_CONCLUSIONS.includes(checkRun.conclusion)) { + reportedFailure = true; + if (checkRun.conclusion === 'CANCELLED') { + cancelledJobs.push({ + name: checkRun.name, + conclusion: checkRun.conclusion, + url: checkRun.detailsUrl + }); + } else { + failedJobs.push({ + name: checkRun.name, + conclusion: checkRun.conclusion, + url: checkRun.detailsUrl + }); } } - } else { - // Fallback to check suite level information if no checkRuns - if (conclusion === 'CANCELLED') { - cancelledJobs.push({ - name: GITHUB_ACTIONS_APP, - conclusion, - url: null - }); - } else { - failedJobs.push({ - name: GITHUB_ACTIONS_APP, - conclusion, - url: null - }); - } + } + + // Fall back to the suite when its failed conclusion is not reflected + // by an individual check run. + if (!reportedFailure) { + const failures = conclusion === 'CANCELLED' + ? cancelledJobs + : failedJobs; + failures.push({ + name: GITHUB_ACTIONS_APP, + conclusion, + url: null + }); } } } diff --git a/lib/queries/PR.gql b/lib/queries/PR.gql index 10400af4..1aa7bdb2 100644 --- a/lib/queries/PR.gql +++ b/lib/queries/PR.gql @@ -38,6 +38,7 @@ query PR($prid: Int!, $owner: String!, $repo: String!) { conclusion, status, checkRuns(first: 40) { + totalCount nodes { name status diff --git a/test/unit/graphql_queries.test.js b/test/unit/graphql_queries.test.js index 39a31bcc..867f29d1 100644 --- a/test/unit/graphql_queries.test.js +++ b/test/unit/graphql_queries.test.js @@ -20,6 +20,7 @@ describe('GraphQL queries', () => { headCommitQuery, /checkSuites\(first: 100, filterBy: \{ appId: 15368 \}\)/); assert.match(headCommitQuery, /checkRuns\(first: 40\)/); + assert.match(headCommitQuery, /checkRuns\(first: 40\) \{\s+totalCount/); assert.match(headCommitQuery, /status \{\s+state\s+\}/); assert.doesNotMatch(headCommitQuery, /\bapp\s*\{/); assert.doesNotMatch(commitsQuery, /checkSuites/); diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index a64c4855..7ae9d5be 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -1860,6 +1860,88 @@ describe('PRChecker', () => { cli.assertCalledWith(expectedLogs); }); + it('should ignore startup failures with only skipped check runs', + async() => { + const cli = new TestCLI(); + + const expectedLogs = { + ok: [ + ['Last GitHub CI successful'] + ] + }; + + const commits = [{ + commit: { + checkSuites: { + nodes: [{ + status: 'COMPLETED', + conclusion: 'STARTUP_FAILURE', + checkRuns: { + totalCount: 2, + nodes: [ + { + name: 'stale-comment', + status: 'COMPLETED', + conclusion: 'SKIPPED' + }, + { + name: 'notable-change', + status: 'COMPLETED', + conclusion: 'SKIPPED' + } + ] + } + }] + } + } + }]; + const data = Object.assign({}, baseData, { commits }); + + const checker = new PRChecker(cli, data, {}, testArgv); + + const status = await checker.checkCI(); + assert(status); + cli.assertCalledWith(expectedLogs); + }); + + it('should report a suite failure when check runs are incomplete', + async() => { + const cli = new TestCLI(); + + const expectedLogs = { + error: [ + ['1 GitHub CI job(s) failed:'], + [' - github-actions: STARTUP_FAILURE'] + ] + }; + + const commits = [{ + commit: { + checkSuites: { + nodes: [{ + status: 'COMPLETED', + conclusion: 'STARTUP_FAILURE', + checkRuns: { + totalCount: 2, + nodes: [{ + name: 'stale-comment', + status: 'COMPLETED', + conclusion: 'SKIPPED' + }] + } + }] + } + } + }]; + const data = Object.assign({}, baseData, { commits }); + + const checker = new PRChecker(cli, data, {}, testArgv); + + const status = await checker.checkCI(); + assert(!status); + cli.assertCalledWith(expectedLogs); + }); + it('should succeed if commit status succeeded', async() => { const cli = new TestCLI(); From 517f5b874511042e7bcaef95f04bc37923190ce2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 15 Sep 2026 17:28:18 +0200 Subject: [PATCH 6/7] fix: handle unavailable Jenkins commit build data (#1193) Signed-off-by: Filip Skokan --- lib/ci/build-types/pr_build.js | 12 +++++++++- test/unit/ci_result_parser.test.js | 36 ++++++++++++++++++++++++++++++ test/unit/pr_checker.test.js | 32 ++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/ci/build-types/pr_build.js b/lib/ci/build-types/pr_build.js index 13f3e5fb..87ccdd41 100644 --- a/lib/ci/build-types/pr_build.js +++ b/lib/ci/build-types/pr_build.js @@ -1,4 +1,5 @@ import { PR_TREE } from '../jenkins_constants.js'; +import { statusType } from '../ci_utils.js'; import CIFailureParser from '../ci_failure_parser.js'; import { TestBuild } from './test_build.js'; import { CommitBuild } from './commit_build.js'; @@ -69,7 +70,7 @@ export class PRBuild extends TestBuild { // Get result from the sub build // assert.strictEqual(commitBuild.jobName, 'node-test-commit'); - const allBuilds = commitBuild.build.subBuilds; + const allBuilds = commitBuild.build?.subBuilds; // TODO: fetch result, builtOn, timestamp in the commit build's own data // ..or maybe they do not worth an additional API call? // Note that we have to pass the actions down to detect resume builds. @@ -78,6 +79,15 @@ export class PRBuild extends TestBuild { }; const commitBuildId = commitBuild.buildNumber; this.commitBuild = new CommitBuild(cli, request, commitBuildId, this.skipMoreThan); + // Jenkins can retain the result after the nested build is no longer available. + if (!commitBuild.build) { + this.commitBuild.setBuildData(buildData); + this.failures = result === statusType.SUCCESS + ? [] + : [new NCUFailure({ url: this.commitBuild.jobUrl }, 'Build data is unavailable')]; + this.commitBuild.failures = this.failures; + return { result, builds: this.builds, failures: this.failures }; + } const { builds, failures } = await this.commitBuild.getResults(buildData); // Set up aliases for display diff --git a/test/unit/ci_result_parser.test.js b/test/unit/ci_result_parser.test.js index 2450bcc5..0ddcbaa9 100644 --- a/test/unit/ci_result_parser.test.js +++ b/test/unit/ci_result_parser.test.js @@ -94,6 +94,42 @@ describe('Jenkins', () => { assert.strictEqual(markdown, expected); }); + for (const result of ['SUCCESS', 'FAILURE', null]) { + it(`should handle unavailable commit build data with result ${result}`, async(t) => { + const data = fixtures.readJSON( + 'jenkins', 'success', 'node-test-pull-request-15237.json' + ); + data.result = result; + data.subBuilds[0].result = result; + data.subBuilds[0].build = null; + + const cli = new TestCLI(); + const prBuild = new PRBuild(cli, {}, 15237); + t.mock.method(prBuild, 'getBuildData', async() => data); + const results = await prBuild.getResults(); + + assert.strictEqual(results.result, result); + assert.deepStrictEqual(results.builds, { + failed: [], aborted: [], pending: [], unstable: [] + }); + assert.strictEqual(prBuild.commitBuild.result, result); + assert.deepStrictEqual(prBuild.commitBuild.failures, prBuild.failures); + if (result === 'SUCCESS') { + assert.deepStrictEqual(results.failures, []); + assert.deepStrictEqual(prBuild.formatAsJson(), []); + assert.strictEqual(prBuild.formatAsMarkdown(), `Job ${prBuild.jobUrl} is green.`); + } else { + assert.strictEqual(results.failures.length, 1); + assert.strictEqual(results.failures[0].type, 'NCU_FAILURE'); + assert.strictEqual(results.failures[0].url, prBuild.commitBuild.jobUrl); + assert.strictEqual(results.failures[0].reason, 'Build data is unavailable'); + assert.match(prBuild.formatAsMarkdown(), /Build data is unavailable/); + assert.strictEqual(prBuild.formatAsJson()[0].reason, 'Build data is unavailable'); + } + prBuild.display(); + }); + } + it('should handle node-test-commit trigger failure', async() => { tmpdir.refresh(); const prefix = ['jenkins', 'trigger-failure']; diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index 7ae9d5be..6ef7a6f0 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -1386,6 +1386,38 @@ describe('PRChecker', () => { }); }); + for (const result of ['SUCCESS', 'FAILURE', null]) { + it(`should check CI with unavailable commit build data and result ${result}`, async() => { + const jenkins = structuredClone(jenkinsCI['success/node-test-pull-request-15237']); + jenkins.result = result; + jenkins.subBuilds[0].result = result; + jenkins.subBuilds[0].build = null; + const data = { + pr: firstTimerPR, + comments: commentsWithSuccessCI, + reviews: approvingReviews, + commits: githubCI['check-suite-success'], + getThread() { + return PRData.prototype.getThread.call(this); + } + }; + const checker = new PRChecker(new TestCLI(), data, { + async json(url) { + assert(url.startsWith(jenkins.url)); + return jenkins; + } + }, argv); + + assert.strictEqual(Boolean(await checker.checkCI()), result === 'SUCCESS'); + assert.deepStrictEqual(checker.reasons.map(({ code }) => code), + result === 'SUCCESS' + ? [] + : [result === null + ? PR_CHECK_REASON_CODES.PENDING_JENKINS_CI + : PR_CHECK_REASON_CODES.FAILED_JENKINS_CI]); + }); + } + it('should error if failed Jenkins CI', async() => { const cli = new TestCLI(); From 42e7103a5ea23bcf135c8ce248838de04b082c56 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 15 Sep 2026 11:36:03 -0400 Subject: [PATCH 7/7] chore(main): release 7.2.2 (#1194) --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58710f2a..8bbc056b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [7.2.2](https://github.com/nodejs/node-core-utils/compare/v7.2.1...v7.2.2) (2026-09-15) + + +### Bug Fixes + +* exclude stale matrix runs from CI reports ([#1191](https://github.com/nodejs/node-core-utils/issues/1191)) ([98b8e3b](https://github.com/nodejs/node-core-utils/commit/98b8e3be26228ca845396bfc33eb21ed67196664)) +* handle unavailable Jenkins commit build data ([#1193](https://github.com/nodejs/node-core-utils/issues/1193)) ([517f5b8](https://github.com/nodejs/node-core-utils/commit/517f5b874511042e7bcaef95f04bc37923190ce2)) +* ignore fully skipped startup failures ([#1170](https://github.com/nodejs/node-core-utils/issues/1170)) ([9f665fa](https://github.com/nodejs/node-core-utils/commit/9f665faa719320adf57f5aae6934cc3e678a601a)) + ## [7.2.1](https://github.com/nodejs/node-core-utils/compare/v7.2.0...v7.2.1) (2026-09-09) diff --git a/package-lock.json b/package-lock.json index fb022103..866823b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@node-core/utils", - "version": "7.2.1", + "version": "7.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@node-core/utils", - "version": "7.2.1", + "version": "7.2.2", "bundleDependencies": [ "@inquirer/prompts", "@listr2/prompt-adapter-inquirer", diff --git a/package.json b/package.json index fefe8280..627f4044 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@node-core/utils", - "version": "7.2.1", + "version": "7.2.2", "description": "Utilities for Node.js core collaborators", "type": "module", "engines": {