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": {