fix: duplicate descriptions (#56)

* fix: duplicate descriptions

* tidy code
This commit is contained in:
Long Tran
2025-02-08 23:33:02 +11:00
committed by GitHub
parent 8a6469b6e4
commit 2be2dfa068
5 changed files with 1297 additions and 446 deletions
+1
View File
@@ -2,3 +2,4 @@ npm exec lint-staged
npm run lint:ec
npm run test:ci
npm run build
git add dist
+287 -81
View File
@@ -1141,6 +1141,121 @@ var require_errors = __commonJS({
}
});
// node_modules/undici/lib/core/constants.js
var require_constants = __commonJS({
"node_modules/undici/lib/core/constants.js"(exports2, module2) {
"use strict";
var headerNameLowerCasedRecord = {};
var wellknownHeaderNames = [
"Accept",
"Accept-Encoding",
"Accept-Language",
"Accept-Ranges",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Origin",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
"Age",
"Allow",
"Alt-Svc",
"Alt-Used",
"Authorization",
"Cache-Control",
"Clear-Site-Data",
"Connection",
"Content-Disposition",
"Content-Encoding",
"Content-Language",
"Content-Length",
"Content-Location",
"Content-Range",
"Content-Security-Policy",
"Content-Security-Policy-Report-Only",
"Content-Type",
"Cookie",
"Cross-Origin-Embedder-Policy",
"Cross-Origin-Opener-Policy",
"Cross-Origin-Resource-Policy",
"Date",
"Device-Memory",
"Downlink",
"ECT",
"ETag",
"Expect",
"Expect-CT",
"Expires",
"Forwarded",
"From",
"Host",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
"Keep-Alive",
"Last-Modified",
"Link",
"Location",
"Max-Forwards",
"Origin",
"Permissions-Policy",
"Pragma",
"Proxy-Authenticate",
"Proxy-Authorization",
"RTT",
"Range",
"Referer",
"Referrer-Policy",
"Refresh",
"Retry-After",
"Sec-WebSocket-Accept",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version",
"Server",
"Server-Timing",
"Service-Worker-Allowed",
"Service-Worker-Navigation-Preload",
"Set-Cookie",
"SourceMap",
"Strict-Transport-Security",
"Supports-Loading-Mode",
"TE",
"Timing-Allow-Origin",
"Trailer",
"Transfer-Encoding",
"Upgrade",
"Upgrade-Insecure-Requests",
"User-Agent",
"Vary",
"Via",
"WWW-Authenticate",
"X-Content-Type-Options",
"X-DNS-Prefetch-Control",
"X-Frame-Options",
"X-Permitted-Cross-Domain-Policies",
"X-Powered-By",
"X-Requested-With",
"X-XSS-Protection"
];
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
const key = wellknownHeaderNames[i];
const lowerCasedKey = key.toLowerCase();
headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = lowerCasedKey;
}
Object.setPrototypeOf(headerNameLowerCasedRecord, null);
module2.exports = {
wellknownHeaderNames,
headerNameLowerCasedRecord
};
}
});
// node_modules/undici/lib/core/util.js
var require_util = __commonJS({
"node_modules/undici/lib/core/util.js"(exports2, module2) {
@@ -1154,6 +1269,7 @@ var require_util = __commonJS({
var { Blob: Blob2 } = require("buffer");
var nodeUtil = require("util");
var { stringify: stringify2 } = require("querystring");
var { headerNameLowerCasedRecord } = require_constants();
var [nodeMajor, nodeMinor] = process.versions.node.split(".").map((v) => Number(v));
function nop() {
}
@@ -1297,6 +1413,9 @@ var require_util = __commonJS({
const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR);
return m ? parseInt(m[1], 10) * 1e3 : null;
}
function headerNameToString(value) {
return headerNameLowerCasedRecord[value] || value.toLowerCase();
}
function parseHeaders(headers, obj = {}) {
if (!Array.isArray(headers))
return headers;
@@ -1500,6 +1619,7 @@ var require_util = __commonJS({
isIterable,
isAsyncIterable,
isDestroyed,
headerNameToString,
parseRawHeaders,
parseHeaders,
parseKeepAliveTimeout,
@@ -3595,7 +3715,7 @@ var require_main = __commonJS({
});
// node_modules/undici/lib/fetch/constants.js
var require_constants = __commonJS({
var require_constants2 = __commonJS({
"node_modules/undici/lib/fetch/constants.js"(exports2, module2) {
"use strict";
var { MessageChannel, receiveMessageOnPort } = require("worker_threads");
@@ -3833,15 +3953,18 @@ var require_global = __commonJS({
var require_util2 = __commonJS({
"node_modules/undici/lib/fetch/util.js"(exports2, module2) {
"use strict";
var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants();
var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants2();
var { getGlobalOrigin } = require_global();
var { performance: performance2 } = require("perf_hooks");
var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util();
var assert = require("assert");
var { isUint8Array: isUint8Array3 } = require("util/types");
var supportedHashes = [];
var crypto4;
try {
crypto4 = require("crypto");
const possibleRelevantHashes = ["sha256", "sha384", "sha512"];
supportedHashes = crypto4.getHashes().filter((hash) => possibleRelevantHashes.includes(hash));
} catch {
}
function responseURL(response) {
@@ -4117,45 +4240,37 @@ var require_util2 = __commonJS({
if (parsedMetadata.length === 0) {
return true;
}
const list4 = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo));
const strongest = list4[0].algo;
const metadata = list4.filter((item) => item.algo === strongest);
const strongest = getStrongestMetadata(parsedMetadata);
const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest);
for (const item of metadata) {
const algorithm = item.algo;
let expectedValue = item.hash;
if (expectedValue.endsWith("==")) {
expectedValue = expectedValue.slice(0, -2);
}
const expectedValue = item.hash;
let actualValue = crypto4.createHash(algorithm).update(bytes).digest("base64");
if (actualValue.endsWith("==")) {
actualValue = actualValue.slice(0, -2);
if (actualValue[actualValue.length - 1] === "=") {
if (actualValue[actualValue.length - 2] === "=") {
actualValue = actualValue.slice(0, -2);
} else {
actualValue = actualValue.slice(0, -1);
}
}
if (actualValue === expectedValue) {
return true;
}
let actualBase64URL = crypto4.createHash(algorithm).update(bytes).digest("base64url");
if (actualBase64URL.endsWith("==")) {
actualBase64URL = actualBase64URL.slice(0, -2);
}
if (actualBase64URL === expectedValue) {
if (compareBase64Mixed(actualValue, expectedValue)) {
return true;
}
}
return false;
}
var parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i;
var parseHashWithOptions = /(?<algo>sha256|sha384|sha512)-((?<hash>[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i;
function parseMetadata(metadata) {
const result = [];
let empty2 = true;
const supportedHashes = crypto4.getHashes();
for (const token of metadata.split(" ")) {
empty2 = false;
const parsedToken = parseHashWithOptions.exec(token);
if (parsedToken === null || parsedToken.groups === void 0) {
if (parsedToken === null || parsedToken.groups === void 0 || parsedToken.groups.algo === void 0) {
continue;
}
const algorithm = parsedToken.groups.algo;
if (supportedHashes.includes(algorithm.toLowerCase())) {
const algorithm = parsedToken.groups.algo.toLowerCase();
if (supportedHashes.includes(algorithm)) {
result.push(parsedToken.groups);
}
}
@@ -4164,6 +4279,51 @@ var require_util2 = __commonJS({
}
return result;
}
function getStrongestMetadata(metadataList) {
let algorithm = metadataList[0].algo;
if (algorithm[3] === "5") {
return algorithm;
}
for (let i = 1; i < metadataList.length; ++i) {
const metadata = metadataList[i];
if (metadata.algo[3] === "5") {
algorithm = "sha512";
break;
} else if (algorithm[3] === "3") {
continue;
} else if (metadata.algo[3] === "3") {
algorithm = "sha384";
}
}
return algorithm;
}
function filterMetadataListByAlgorithm(metadataList, algorithm) {
if (metadataList.length === 1) {
return metadataList;
}
let pos = 0;
for (let i = 0; i < metadataList.length; ++i) {
if (metadataList[i].algo === algorithm) {
metadataList[pos++] = metadataList[i];
}
}
metadataList.length = pos;
return metadataList;
}
function compareBase64Mixed(actualValue, expectedValue) {
if (actualValue.length !== expectedValue.length) {
return false;
}
for (let i = 0; i < actualValue.length; ++i) {
if (actualValue[i] !== expectedValue[i]) {
if (actualValue[i] === "+" && expectedValue[i] === "-" || actualValue[i] === "/" && expectedValue[i] === "_") {
continue;
}
return false;
}
}
return true;
}
function tryUpgradeRequestToAPotentiallyTrustworthyURL(request) {
}
function sameOrigin(A, B) {
@@ -4387,7 +4547,8 @@ var require_util2 = __commonJS({
urlHasHttpsScheme,
urlIsHttpHttpsScheme,
readAllBytes,
normalizeMethodRecord
normalizeMethodRecord,
parseMetadata
};
}
});
@@ -5424,7 +5585,7 @@ var require_body = __commonJS({
var { FormData } = require_formdata();
var { kState } = require_symbols2();
var { webidl } = require_webidl();
var { DOMException: DOMException2, structuredClone } = require_constants();
var { DOMException: DOMException2, structuredClone } = require_constants2();
var { Blob: Blob2, File: NativeFile } = require("buffer");
var { kBodyUsed } = require_symbols();
var assert = require("assert");
@@ -5432,6 +5593,13 @@ var require_body = __commonJS({
var { isUint8Array: isUint8Array3, isArrayBuffer } = require("util/types");
var { File: UndiciFile } = require_file();
var { parseMIMEType, serializeAMimeType } = require_dataURL();
var random;
try {
const crypto4 = require("node:crypto");
random = (max) => crypto4.randomInt(0, max);
} catch {
random = (max) => Math.floor(Math.random(max));
}
var ReadableStream = globalThis.ReadableStream;
var File = NativeFile ?? UndiciFile;
var textEncoder = new TextEncoder();
@@ -5474,7 +5642,7 @@ var require_body = __commonJS({
} else if (ArrayBuffer.isView(object2)) {
source = new Uint8Array(object2.buffer.slice(object2.byteOffset, object2.byteOffset + object2.byteLength));
} else if (util2.isFormDataLike(object2)) {
const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, "0")}`;
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, "0")}`;
const prefix = `--${boundary}\r
Content-Disposition: form-data`;
const escape = (str) => str.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22");
@@ -6520,7 +6688,7 @@ var require_utils2 = __commonJS({
});
// node_modules/undici/lib/llhttp/constants.js
var require_constants2 = __commonJS({
var require_constants3 = __commonJS({
"node_modules/undici/lib/llhttp/constants.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
@@ -6955,7 +7123,17 @@ var require_RedirectHandler = __commonJS({
}
}
function shouldRemoveHeader(header, removeContent, unknownOrigin) {
return header.length === 4 && header.toString().toLowerCase() === "host" || removeContent && header.toString().toLowerCase().indexOf("content-") === 0 || unknownOrigin && header.length === 13 && header.toString().toLowerCase() === "authorization" || unknownOrigin && header.length === 6 && header.toString().toLowerCase() === "cookie";
if (header.length === 4) {
return util2.headerNameToString(header) === "host";
}
if (removeContent && util2.headerNameToString(header).startsWith("content-")) {
return true;
}
if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
const name = util2.headerNameToString(header);
return name === "authorization" || name === "cookie" || name === "proxy-authorization";
}
return false;
}
function cleanRequestHeaders(headers, removeContent, unknownOrigin) {
const ret = [];
@@ -7400,7 +7578,7 @@ var require_client = __commonJS({
);
resume(client);
}
var constants = require_constants2();
var constants = require_constants3();
var createRedirectInterceptor = require_redirectInterceptor();
var EMPTY_BUF = Buffer.alloc(0);
async function lazyllhttp() {
@@ -12104,7 +12282,7 @@ var require_response = __commonJS({
redirectStatusSet,
nullBodyStatus,
DOMException: DOMException2
} = require_constants();
} = require_constants2();
var { kState, kHeaders, kGuard, kRealm } = require_symbols2();
var { webidl } = require_webidl();
var { FormData } = require_formdata();
@@ -12486,7 +12664,7 @@ var require_request2 = __commonJS({
requestCredentials,
requestCache,
requestDuplex
} = require_constants();
} = require_constants2();
var { kEnumerableProperty } = util2;
var { kHeaders, kSignal, kState, kGuard, kRealm } = require_symbols2();
var { webidl } = require_webidl();
@@ -13155,7 +13333,7 @@ var require_fetch = __commonJS({
requestBodyHeader,
subresourceSet,
DOMException: DOMException2
} = require_constants();
} = require_constants2();
var { kHeadersList } = require_symbols();
var EE = require("events");
var { Readable, pipeline } = require("stream");
@@ -14519,7 +14697,7 @@ var require_util4 = __commonJS({
} = require_symbols3();
var { ProgressEvent } = require_progressevent();
var { getEncoding } = require_encoding();
var { DOMException: DOMException2 } = require_constants();
var { DOMException: DOMException2 } = require_constants2();
var { serializeAMimeType, parseMIMEType } = require_dataURL();
var { types } = require("util");
var { StringDecoder } = require("string_decoder");
@@ -15635,7 +15813,7 @@ var require_cachestorage = __commonJS({
});
// node_modules/undici/lib/cookies/constants.js
var require_constants3 = __commonJS({
var require_constants4 = __commonJS({
"node_modules/undici/lib/cookies/constants.js"(exports2, module2) {
"use strict";
var maxAttributeValueSize = 1024;
@@ -15810,7 +15988,7 @@ var require_util6 = __commonJS({
var require_parse = __commonJS({
"node_modules/undici/lib/cookies/parse.js"(exports2, module2) {
"use strict";
var { maxNameValuePairSize, maxAttributeValueSize } = require_constants3();
var { maxNameValuePairSize, maxAttributeValueSize } = require_constants4();
var { isCTLExcludingHtab } = require_util6();
var { collectASequenceOfCodePointsFast } = require_dataURL();
var assert = require("assert");
@@ -16075,7 +16253,7 @@ var require_cookies = __commonJS({
});
// node_modules/undici/lib/websocket/constants.js
var require_constants4 = __commonJS({
var require_constants5 = __commonJS({
"node_modules/undici/lib/websocket/constants.js"(exports2, module2) {
"use strict";
var uid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
@@ -16383,7 +16561,7 @@ var require_util7 = __commonJS({
"node_modules/undici/lib/websocket/util.js"(exports2, module2) {
"use strict";
var { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require_symbols5();
var { states, opcodes } = require_constants4();
var { states, opcodes } = require_constants5();
var { MessageEvent, ErrorEvent } = require_events();
function isEstablished(ws) {
return ws[kReadyState] === states.OPEN;
@@ -16473,7 +16651,7 @@ var require_connection = __commonJS({
"node_modules/undici/lib/websocket/connection.js"(exports2, module2) {
"use strict";
var diagnosticsChannel = require("diagnostics_channel");
var { uid, states } = require_constants4();
var { uid, states } = require_constants5();
var {
kReadyState,
kSentClose,
@@ -16620,7 +16798,7 @@ var require_connection = __commonJS({
var require_frame = __commonJS({
"node_modules/undici/lib/websocket/frame.js"(exports2, module2) {
"use strict";
var { maxUnsigned16Bit } = require_constants4();
var { maxUnsigned16Bit } = require_constants5();
var crypto4;
try {
crypto4 = require("crypto");
@@ -16679,7 +16857,7 @@ var require_receiver = __commonJS({
"use strict";
var { Writable } = require("stream");
var diagnosticsChannel = require("diagnostics_channel");
var { parserStates, opcodes, states, emptyBuffer } = require_constants4();
var { parserStates, opcodes, states, emptyBuffer } = require_constants5();
var { kReadyState, kSentClose, kResponse, kReceivedClose } = require_symbols5();
var { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require_util7();
var { WebsocketFrameSend } = require_frame();
@@ -16914,10 +17092,10 @@ var require_websocket = __commonJS({
"node_modules/undici/lib/websocket/websocket.js"(exports2, module2) {
"use strict";
var { webidl } = require_webidl();
var { DOMException: DOMException2 } = require_constants();
var { DOMException: DOMException2 } = require_constants2();
var { URLSerializer } = require_dataURL();
var { getGlobalOrigin } = require_global();
var { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require_constants4();
var { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require_constants5();
var {
kWebSocketURL,
kReadyState,
@@ -43036,6 +43214,7 @@ var remark2 = remark().use(remarkGfm).data("settings", {
// src/main.ts
var ANCHOR = "<!-- branch-stack -->";
var PULL_REQUEST_NODE_REGEX = /#\d+ :point_left:/;
async function main({
octokit,
currentPullRequest,
@@ -43182,58 +43361,85 @@ function getOutput(graph, terminatingRefs) {
);
return lines.join("\n");
}
var findInlineAnchorIndex = (descriptionAst) => {
const listChildren = descriptionAst.children.map((node2, originalIndex) => [node2, originalIndex]).filter(([node2]) => node2.type === "list");
const [, listChildWithAnchorIdx] = listChildren.find(([node2]) => {
const listItems = node2.children;
const maybeFirstListItemParagraph = listItems[0]?.children[0];
return maybeFirstListItemParagraph?.children.some(
(node3) => node3.type === "html" && node3.value === ANCHOR
);
}) ?? [];
return listChildWithAnchorIdx;
};
var isListType = (listAstNode) => listAstNode?.type === "list";
var nextChildIsListAndContainsPrs = (listAst) => {
if (!listAst || !isListType(listAst))
return false;
if (listAst.children.length > 1) {
return false;
}
const subList = listAst.children[0]?.children[1];
if (!isListType(subList))
return false;
const firstItemParagraphNode = subList.children[0]?.children[0];
if (firstItemParagraphNode?.type !== "paragraph")
return false;
const sublistFirstItemParagraphText = firstItemParagraphNode.children[0];
if (sublistFirstItemParagraphText?.type !== "text")
return false;
return /^#\d+/.test(sublistFirstItemParagraphText.value);
};
function updateDescription({
description,
output
}) {
const descriptionAst = remark2.parse(description);
const outputAst = remark2.parse(output);
const inlineAnchorIndex = findInlineAnchorIndex(descriptionAst);
if (inlineAnchorIndex) {
descriptionAst.children.splice(inlineAnchorIndex, 1, ...outputAst.children);
return remark2.stringify(descriptionAst);
}
const anchorIndex = descriptionAst.children.findIndex(
const standaloneAnchorIndex = descriptionAst.children.findIndex(
(node2) => node2.type === "html" && node2.value === ANCHOR
);
const isMissingAnchor = anchorIndex === -1;
if (standaloneAnchorIndex >= 0) {
removeUnanchoredBranchStack(descriptionAst);
descriptionAst.children.splice(standaloneAnchorIndex, 1, ...outputAst.children);
return remark2.stringify(descriptionAst);
}
const inlineAnchorIndex = findInlineAnchor(descriptionAst);
const isMissingAnchor = inlineAnchorIndex === -1;
if (isMissingAnchor) {
removeUnanchoredBranchStack(descriptionAst);
descriptionAst.children.push(...outputAst.children);
return remark2.stringify(descriptionAst);
}
const numChildrenToReplace = nextChildIsListAndContainsPrs(descriptionAst.children[anchorIndex + 1]) ? 2 : 1;
descriptionAst.children.splice(anchorIndex, numChildrenToReplace, ...outputAst.children);
descriptionAst.children.splice(inlineAnchorIndex, 1, ...outputAst.children);
return remark2.stringify(descriptionAst);
}
function removeUnanchoredBranchStack(descriptionAst) {
const branchStackIndex = descriptionAst.children.findIndex(
function matchesBranchStackHeuristic(node2) {
if (node2.type !== "list") {
return false;
}
const child = node2.children[0];
if (node2.children.length !== 1 || !child) {
return false;
}
const result = containsPullRequestNode(child);
return result;
}
);
if (branchStackIndex === -1) {
return;
}
descriptionAst.children.splice(branchStackIndex, 1);
}
function containsPullRequestNode(listItem2) {
return listItem2.children.some((node2) => {
if (node2.type === "list" && node2.children.length > 0) {
return node2.children.some(containsPullRequestNode);
}
if (node2.type !== "paragraph") {
return false;
}
const result = node2.children.some(
(child) => child.type === "text" && PULL_REQUEST_NODE_REGEX.test(child.value)
);
return result;
});
}
function findInlineAnchor(descriptionAst) {
return descriptionAst.children.findIndex((node2) => {
if (node2.type !== "list") {
return;
}
return node2.children.some(containsAnchor);
});
}
function containsAnchor(listItem2) {
return listItem2.children.some((node2) => {
if (node2.type === "list") {
return node2.children.some(containsAnchor);
}
if (node2.type !== "paragraph") {
return false;
}
const result = node2.children.some(
(child) => child.type === "html" && child.value === ANCHOR
);
return result;
});
}
// src/inputs.ts
var core2 = __toESM(require_core());
+806 -216
View File
File diff suppressed because it is too large Load Diff
+117 -91
View File
@@ -8,91 +8,111 @@ beforeEach(() => {
})
describe('updateDescription', () => {
it('should correctly update pull request body', () => {
const description = `
## Description
describe('when standalone anchor is present', () => {
describe('when previous stack exists', () => {
it('should delete previous stack & replace anchor with updated stack', () => {
const description = [
'<!-- branch-stack -->',
'',
'- `main`',
' - \\#1 :point\\_left:',
].join('\n')
const output = ['- `main` <!-- branch-stack -->', ' - \\#2 :point\\_left:'].join(
'\n'
)
## Stack
const actual = updateDescription({ description, output })
const expected = [
'- `main` <!-- branch-stack -->',
' - \\#2 :point\\_left:',
'',
].join('\n')
<!-- branch-stack -->
- main
- \\#1
`
const output = ['- main <!-- branch-stack -->', ' - \\#2'].join('\n')
expect(actual).toBe(expected)
})
})
const actual = updateDescription({ description, output })
const expected = [
'## Description',
'',
'## Stack',
'',
'- main <!-- branch-stack -->',
' - \\#2',
'',
].join('\n')
describe('when stack is missing', () => {
it('should replace standalone anchor with updated stack', () => {
const description = ['<!-- branch-stack -->', ''].join('\n')
const output = ['- main <!-- branch-stack -->', ' - \\#2 :point\\_left:'].join(
'\n'
)
expect(actual).toEqual(expected)
const actual = updateDescription({ description, output })
const expected = [
'- main <!-- branch-stack -->',
' - \\#2 :point\\_left:',
'',
].join('\n')
expect(actual).toEqual(expected)
})
})
})
it('should correctly update pull request body when the comment is inline', () => {
const description = `
## Description
describe('when inline anchor is present', () => {
it('should replace inline anchor with updated stack', () => {
const description = [
'- `main` <!-- branch-stack -->',
' - \\#1 :point_left:',
'',
].join('\n')
const output = ['- `main` <!-- branch-stack -->', ' - \\#2 :point\\_left:'].join(
'\n'
)
## Stack
const actual = updateDescription({ description, output })
const expected = [
'- `main` <!-- branch-stack -->',
' - \\#2 :point\\_left:',
'',
].join('\n')
- main <!-- branch-stack -->
- \\#2
`
const output = ['- main <!-- branch-stack -->', ' - \\#2'].join('\n')
const actual = updateDescription({ description, output })
const expected = [
'## Description',
'',
'## Stack',
'',
'- main <!-- branch-stack -->',
' - \\#2',
'',
].join('\n')
expect(actual).toEqual(expected)
expect(actual).toBe(expected)
})
})
it('should append output to description if body regex fails', () => {
const description = '## Description'
const output = ['- main <!-- branch-stack -->', ' - \\#2'].join('\n')
describe('when anchor is missing', () => {
describe('when previous stack exists', () => {
it('should replace previous stack with updated stack', () => {
const description = ['- `main`', ' - \\#1 :point\\_left:', ''].join('\n')
const output = ['- `main` <!-- branch-stack -->', ' - \\#2 :point\\_left:'].join(
'\n'
)
const actual = updateDescription({ description, output })
const expected = [
'## Description',
'',
'- main <!-- branch-stack -->',
' - \\#2',
'',
].join('\n')
const actual = updateDescription({ description, output })
const expected = [
'- `main` <!-- branch-stack -->',
' - \\#2 :point\\_left:',
'',
].join('\n')
expect(actual).toEqual(expected)
expect(actual).toBe(expected)
})
})
})
it('should not delete parts of the description below itself when there is another list', () => {
const description = `<!-- branch-stack -->
## More Description
There may be things here we don't want to overwrite.
- [ ] including
- [ ] something
- [ ] like a
- [ ] checklist
`
const output = ['- main <!-- branch-stack -->', ' - \\#1'].join('\n')
const description = [
'<!-- branch-stack -->',
'',
'## More Description',
'',
`There may be things here we don't want to overwrite.`,
'',
'- [ ] including',
'- [ ] something',
'- [ ] like a',
'- [ ] checklist',
'',
].join('\n')
const output = ['- main <!-- branch-stack -->', ' - \\#1 :point\\_left:'].join('\n')
const actual = updateDescription({ description, output })
const expected = [
'- main <!-- branch-stack -->',
' - \\#1',
' - \\#1 :point\\_left:',
'',
'## More Description',
'',
@@ -109,19 +129,21 @@ There may be things here we don't want to overwrite.
})
it('should not replace any list directly succeeding the stack comment', () => {
const description = `<!-- branch-stack -->
- [ ] this checklist
- [ ] is going to be alright but with an asterisk to start
## More Description
`
const output = ['- main <!-- branch-stack -->', ' - \\#1'].join('\n')
const description = [
'<!-- branch-stack -->',
'',
'- [ ] this checklist',
' - [ ] is going to be alright but with an asterisk to start',
'',
'## More Description',
'',
].join('\n')
const output = ['- main <!-- branch-stack -->', ' - \\#1 :point\\_left:'].join('\n')
const actual = updateDescription({ description, output })
const expected = [
'- main <!-- branch-stack -->',
' - \\#1',
' - \\#1 :point\\_left:',
'',
'* [ ] this checklist',
' - [ ] is going to be alright but with an asterisk to start',
@@ -134,18 +156,19 @@ There may be things here we don't want to overwrite.
})
it('should correctly update pull request body when the comment is inline and there is a succeeding list', () => {
const description = `
## Description
## Stack
- main <!-- branch-stack -->
- \\#1
* my list
- should survive
`
const output = ['- main <!-- branch-stack -->', ' - \\#2'].join('\n')
const description = [
'## Description',
'',
'## Stack',
'',
'- main <!-- branch-stack -->',
' - \\#1 :point\\_left:',
'',
'* my list',
' - should survive',
'',
].join('\n')
const output = ['- main <!-- branch-stack -->', ' - \\#2 :point\\_left:'].join('\n')
const actual = updateDescription({ description, output })
const expected = [
@@ -154,7 +177,7 @@ There may be things here we don't want to overwrite.
'## Stack',
'',
'- main <!-- branch-stack -->',
' - \\#2',
' - \\#2 :point\\_left:',
'',
'* my list',
' - should survive',
@@ -275,9 +298,12 @@ describe('getOutput', () => {
const stackGraph = getStackGraph(pullRequest1, repoGraph)
const output = getOutput(stackGraph, ['main'])
const expected = [
'- `main` <!-- branch-stack -->',
' - #1 :point_left:',
' - #2',
].join('\n')
expect(output).toBe(`- \`main\` <!-- branch-stack -->
- #1 :point_left:
- #2`)
expect(output).toBe(expected)
})
})
+86 -58
View File
@@ -3,11 +3,12 @@ import * as github from '@actions/github'
import { DirectedGraph } from 'graphology'
import { bfsFromNode, dfsFromNode } from 'graphology-traversal'
import { topologicalSort } from 'graphology-dag'
import type { BlockContent, DefinitionContent, List, Paragraph, RootContent } from 'mdast'
import type { ListItem, Root } from 'mdast'
import type { PullRequest, Context, StackNodeAttributes } from './types'
import { remark } from './remark'
const ANCHOR = '<!-- branch-stack -->'
export const PULL_REQUEST_NODE_REGEX = /#\d+ :point_left:/
export async function main({
octokit,
@@ -206,49 +207,6 @@ export function getOutput(
return lines.join('\n')
}
const findInlineAnchorIndex = (descriptionAst: ReturnType<typeof remark.parse>) => {
const listChildren = descriptionAst.children
.map((node, originalIndex) => [node, originalIndex] as const)
.filter(([node]) => node.type === 'list')
const [, listChildWithAnchorIdx] =
(listChildren as Array<[List, number]>).find(([node]) => {
const listItems = node.children
const maybeFirstListItemParagraph = listItems[0]?.children[0] as
| Paragraph
| undefined
return maybeFirstListItemParagraph?.children.some(
(node) => node.type === 'html' && node.value === ANCHOR
)
}) ?? []
return listChildWithAnchorIdx
}
const isListType = (
listAstNode: RootContent | undefined | BlockContent | DefinitionContent
): listAstNode is List => listAstNode?.type === 'list'
const nextChildIsListAndContainsPrs = (listAst: RootContent | undefined) => {
if (!listAst || !isListType(listAst)) return false
if (listAst.children.length > 1) {
return false
}
const subList = listAst.children[0]?.children[1]
if (!isListType(subList)) return false
const firstItemParagraphNode = subList.children[0]?.children[0]
if (firstItemParagraphNode?.type !== 'paragraph') return false
const sublistFirstItemParagraphText = firstItemParagraphNode.children[0]
if (sublistFirstItemParagraphText?.type !== 'text') return false
return /^#\d+/.test(sublistFirstItemParagraphText.value)
}
export function updateDescription({
description,
output,
@@ -259,28 +217,98 @@ export function updateDescription({
const descriptionAst = remark.parse(description)
const outputAst = remark.parse(output)
const inlineAnchorIndex = findInlineAnchorIndex(descriptionAst)
if (inlineAnchorIndex) {
descriptionAst.children.splice(inlineAnchorIndex, 1, ...outputAst.children)
return remark.stringify(descriptionAst)
}
const anchorIndex = descriptionAst.children.findIndex(
const standaloneAnchorIndex = descriptionAst.children.findIndex(
(node) => node.type === 'html' && node.value === ANCHOR
)
const isMissingAnchor = anchorIndex === -1
if (isMissingAnchor) {
descriptionAst.children.push(...outputAst.children)
if (standaloneAnchorIndex >= 0) {
removeUnanchoredBranchStack(descriptionAst)
descriptionAst.children.splice(standaloneAnchorIndex, 1, ...outputAst.children)
return remark.stringify(descriptionAst)
}
const numChildrenToReplace =
nextChildIsListAndContainsPrs(descriptionAst.children[anchorIndex + 1]) ? 2 : 1
const inlineAnchorIndex = findInlineAnchor(descriptionAst)
descriptionAst.children.splice(anchorIndex, numChildrenToReplace, ...outputAst.children)
const isMissingAnchor = inlineAnchorIndex === -1
if (isMissingAnchor) {
removeUnanchoredBranchStack(descriptionAst)
descriptionAst.children.push(...outputAst.children)
return remark.stringify(descriptionAst)
}
descriptionAst.children.splice(inlineAnchorIndex, 1, ...outputAst.children)
return remark.stringify(descriptionAst)
}
function removeUnanchoredBranchStack(descriptionAst: Root) {
const branchStackIndex = descriptionAst.children.findIndex(
function matchesBranchStackHeuristic(node) {
if (node.type !== 'list') {
return false
}
const child = node.children[0]
if (node.children.length !== 1 || !child) {
return false
}
const result = containsPullRequestNode(child)
return result
}
)
if (branchStackIndex === -1) {
return
}
descriptionAst.children.splice(branchStackIndex, 1)
}
function containsPullRequestNode(listItem: ListItem) {
return listItem.children.some((node) => {
if (node.type === 'list' && node.children.length > 0) {
return node.children.some(containsPullRequestNode)
}
if (node.type !== 'paragraph') {
return false
}
const result = node.children.some(
(child) => child.type === 'text' && PULL_REQUEST_NODE_REGEX.test(child.value)
)
return result
})
}
function findInlineAnchor(descriptionAst: Root): number {
return descriptionAst.children.findIndex((node) => {
if (node.type !== 'list') {
return
}
return node.children.some(containsAnchor)
})
}
function containsAnchor(listItem: ListItem): boolean {
return listItem.children.some((node) => {
if (node.type === 'list') {
return node.children.some(containsAnchor)
}
if (node.type !== 'paragraph') {
return false
}
const result = node.children.some(
(child) => child.type === 'html' && child.value === ANCHOR
)
return result
})
}