diff --git a/.changeset/patch-remove-duplicate-title.md b/.changeset/patch-remove-duplicate-title.md new file mode 100644 index 00000000000..cd2680971fa --- /dev/null +++ b/.changeset/patch-remove-duplicate-title.md @@ -0,0 +1,7 @@ +--- +"gh-aw": patch +--- + +Add a JavaScript helper that removes duplicate titles from safe output descriptions and register it in the bundler. + +The helper `removeDuplicateTitleFromDescription` is used by create/update scripts for issues, discussions, and pull requests to avoid repeating the title in the description body. diff --git a/.github/workflows/artifacts-summary.lock.yml b/.github/workflows/artifacts-summary.lock.yml index 2407bce3a40..a51bed6e3cb 100644 --- a/.github/workflows/artifacts-summary.lock.yml +++ b/.github/workflows/artifacts-summary.lock.yml @@ -6560,6 +6560,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6744,7 +6763,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/audit-workflows.lock.yml b/.github/workflows/audit-workflows.lock.yml index 8f36fb5cd45..e5bfc987b1b 100644 --- a/.github/workflows/audit-workflows.lock.yml +++ b/.github/workflows/audit-workflows.lock.yml @@ -7382,6 +7382,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7566,7 +7585,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/blog-auditor.lock.yml b/.github/workflows/blog-auditor.lock.yml index 25cb5e80bb1..c5b4eb6ec3c 100644 --- a/.github/workflows/blog-auditor.lock.yml +++ b/.github/workflows/blog-auditor.lock.yml @@ -6428,6 +6428,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6612,7 +6631,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/breaking-change-checker.lock.yml b/.github/workflows/breaking-change-checker.lock.yml index b0d2f76b01e..3fcd7c6e395 100644 --- a/.github/workflows/breaking-change-checker.lock.yml +++ b/.github/workflows/breaking-change-checker.lock.yml @@ -6509,6 +6509,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6641,6 +6660,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index 49405e53947..5357beed264 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -7600,6 +7600,25 @@ jobs: core.setFailed(error instanceof Error ? error : String(error)); } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function resolveTargetNumber(params) { const { updateTarget, item, numberField, isValidContext, contextNumber, displayName } = params; if (updateTarget === "*") { @@ -7640,10 +7659,12 @@ jobs: logMessages.push(`Invalid status value: ${item.status}. Must be 'open' or 'closed'`); } } + let titleForDedup = null; if (canUpdateTitle && item.title !== undefined) { const trimmedTitle = typeof item.title === "string" ? item.title.trim() : ""; if (trimmedTitle.length > 0) { updateData.title = trimmedTitle; + titleForDedup = trimmedTitle; hasUpdates = true; logMessages.push(`Will update title to: ${trimmedTitle}`); } else { @@ -7652,9 +7673,13 @@ jobs: } if (canUpdateBody && item.body !== undefined) { if (typeof item.body === "string") { - updateData.body = item.body; + let processedBody = item.body; + if (titleForDedup) { + processedBody = removeDuplicateTitleFromDescription(titleForDedup, processedBody); + } + updateData.body = processedBody; hasUpdates = true; - logMessages.push(`Will update body (length: ${item.body.length})`); + logMessages.push(`Will update body (length: ${processedBody.length})`); } else { logMessages.push("Invalid body value: must be a string"); } diff --git a/.github/workflows/ci-coach.lock.yml b/.github/workflows/ci-coach.lock.yml index e23e2391b8e..637c23a3711 100644 --- a/.github/workflows/ci-coach.lock.yml +++ b/.github/workflows/ci-coach.lock.yml @@ -7447,6 +7447,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7638,7 +7657,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/ci-doctor.lock.yml b/.github/workflows/ci-doctor.lock.yml index 0688ffa78b5..9954d9908ad 100644 --- a/.github/workflows/ci-doctor.lock.yml +++ b/.github/workflows/ci-doctor.lock.yml @@ -7212,6 +7212,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7344,6 +7363,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/cli-consistency-checker.lock.yml b/.github/workflows/cli-consistency-checker.lock.yml index 068d1b39652..aee2f65af2b 100644 --- a/.github/workflows/cli-consistency-checker.lock.yml +++ b/.github/workflows/cli-consistency-checker.lock.yml @@ -6498,6 +6498,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6630,6 +6649,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/cli-version-checker.lock.yml b/.github/workflows/cli-version-checker.lock.yml index f5d009ef3d4..f9590aeca25 100644 --- a/.github/workflows/cli-version-checker.lock.yml +++ b/.github/workflows/cli-version-checker.lock.yml @@ -6271,6 +6271,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6403,6 +6422,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/cloclo.lock.yml b/.github/workflows/cloclo.lock.yml index 21a1eda678a..f2dd58bd537 100644 --- a/.github/workflows/cloclo.lock.yml +++ b/.github/workflows/cloclo.lock.yml @@ -7647,6 +7647,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7838,7 +7857,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/commit-changes-analyzer.lock.yml b/.github/workflows/commit-changes-analyzer.lock.yml index 879bba0b0ac..489153a2942 100644 --- a/.github/workflows/commit-changes-analyzer.lock.yml +++ b/.github/workflows/commit-changes-analyzer.lock.yml @@ -6302,6 +6302,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6486,7 +6505,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/copilot-agent-analysis.lock.yml b/.github/workflows/copilot-agent-analysis.lock.yml index 41925904724..c194472b3ea 100644 --- a/.github/workflows/copilot-agent-analysis.lock.yml +++ b/.github/workflows/copilot-agent-analysis.lock.yml @@ -7056,6 +7056,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7240,7 +7259,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/copilot-pr-merged-report.lock.yml b/.github/workflows/copilot-pr-merged-report.lock.yml index b6ec6081b61..2bfa5424344 100644 --- a/.github/workflows/copilot-pr-merged-report.lock.yml +++ b/.github/workflows/copilot-pr-merged-report.lock.yml @@ -8055,6 +8055,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8239,7 +8258,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/copilot-pr-nlp-analysis.lock.yml b/.github/workflows/copilot-pr-nlp-analysis.lock.yml index b3cb90ab69b..4c3b8e8b513 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.lock.yml +++ b/.github/workflows/copilot-pr-nlp-analysis.lock.yml @@ -8162,6 +8162,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8346,7 +8365,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/copilot-pr-prompt-analysis.lock.yml b/.github/workflows/copilot-pr-prompt-analysis.lock.yml index dbf04560652..712787e79d6 100644 --- a/.github/workflows/copilot-pr-prompt-analysis.lock.yml +++ b/.github/workflows/copilot-pr-prompt-analysis.lock.yml @@ -7177,6 +7177,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7361,7 +7380,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/copilot-session-insights.lock.yml b/.github/workflows/copilot-session-insights.lock.yml index 689c106dff0..d727c85ec35 100644 --- a/.github/workflows/copilot-session-insights.lock.yml +++ b/.github/workflows/copilot-session-insights.lock.yml @@ -8474,6 +8474,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8658,7 +8677,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-code-metrics.lock.yml b/.github/workflows/daily-code-metrics.lock.yml index bcd70b84d7a..097453a8c9f 100644 --- a/.github/workflows/daily-code-metrics.lock.yml +++ b/.github/workflows/daily-code-metrics.lock.yml @@ -7511,6 +7511,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7695,7 +7714,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-copilot-token-report.lock.yml b/.github/workflows/daily-copilot-token-report.lock.yml index 2560bf4a269..2ad6d034430 100644 --- a/.github/workflows/daily-copilot-token-report.lock.yml +++ b/.github/workflows/daily-copilot-token-report.lock.yml @@ -8331,6 +8331,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8515,7 +8534,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-doc-updater.lock.yml b/.github/workflows/daily-doc-updater.lock.yml index 1f5dc7e62c9..99423d81ca5 100644 --- a/.github/workflows/daily-doc-updater.lock.yml +++ b/.github/workflows/daily-doc-updater.lock.yml @@ -5900,6 +5900,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6091,7 +6110,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/daily-file-diet.lock.yml b/.github/workflows/daily-file-diet.lock.yml index 44daa608e98..2da42d25cdc 100644 --- a/.github/workflows/daily-file-diet.lock.yml +++ b/.github/workflows/daily-file-diet.lock.yml @@ -6290,6 +6290,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6422,6 +6441,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/daily-firewall-report.lock.yml b/.github/workflows/daily-firewall-report.lock.yml index 24776fcbd87..37bd63ed2a7 100644 --- a/.github/workflows/daily-firewall-report.lock.yml +++ b/.github/workflows/daily-firewall-report.lock.yml @@ -7626,6 +7626,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7810,7 +7829,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-issues-report.lock.yml b/.github/workflows/daily-issues-report.lock.yml index c9d59fe196e..c2ccdf72b8a 100644 --- a/.github/workflows/daily-issues-report.lock.yml +++ b/.github/workflows/daily-issues-report.lock.yml @@ -8338,6 +8338,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8522,7 +8541,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index e6baffdf4b7..0a4c0c6a23d 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -5908,6 +5908,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6040,6 +6059,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/daily-news.lock.yml b/.github/workflows/daily-news.lock.yml index 04c9d4e08dd..492e3255f5b 100644 --- a/.github/workflows/daily-news.lock.yml +++ b/.github/workflows/daily-news.lock.yml @@ -8090,6 +8090,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8274,7 +8293,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-performance-summary.lock.yml b/.github/workflows/daily-performance-summary.lock.yml index 9a8ea74c52c..c541ba752a5 100644 --- a/.github/workflows/daily-performance-summary.lock.yml +++ b/.github/workflows/daily-performance-summary.lock.yml @@ -9576,6 +9576,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -9760,7 +9779,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-repo-chronicle.lock.yml b/.github/workflows/daily-repo-chronicle.lock.yml index 7f82d9cadab..7d7d34f4d9c 100644 --- a/.github/workflows/daily-repo-chronicle.lock.yml +++ b/.github/workflows/daily-repo-chronicle.lock.yml @@ -7764,6 +7764,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7948,7 +7967,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-team-status.lock.yml b/.github/workflows/daily-team-status.lock.yml index 99dfd0da733..d2fcfe062ab 100644 --- a/.github/workflows/daily-team-status.lock.yml +++ b/.github/workflows/daily-team-status.lock.yml @@ -6005,381 +6005,13 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const fs = require("fs"); - const crypto = require("crypto"); - const MAX_LOG_CONTENT_LENGTH = 10000; - function truncateForLogging(content) { - if (content.length <= MAX_LOG_CONTENT_LENGTH) { - return content; - } - return content.substring(0, MAX_LOG_CONTENT_LENGTH) + `\n... (truncated, total length: ${content.length})`; - } - function loadAgentOutput() { - const agentOutputFile = process.env.GH_AW_AGENT_OUTPUT; - if (!agentOutputFile) { - core.info("No GH_AW_AGENT_OUTPUT environment variable found"); - return { success: false }; - } - let outputContent; - try { - outputContent = fs.readFileSync(agentOutputFile, "utf8"); - } catch (error) { - const errorMessage = `Error reading agent output file: ${error instanceof Error ? error.message : String(error)}`; - core.error(errorMessage); - return { success: false, error: errorMessage }; - } - if (outputContent.trim() === "") { - core.info("Agent output content is empty"); - return { success: false }; - } - core.info(`Agent output content length: ${outputContent.length}`); - let validatedOutput; - try { - validatedOutput = JSON.parse(outputContent); - } catch (error) { - const errorMessage = `Error parsing agent output JSON: ${error instanceof Error ? error.message : String(error)}`; - core.error(errorMessage); - core.info(`Failed to parse content:\n${truncateForLogging(outputContent)}`); - return { success: false, error: errorMessage }; - } - if (!validatedOutput.items || !Array.isArray(validatedOutput.items)) { - core.info("No valid items found in agent output"); - core.info(`Parsed content: ${truncateForLogging(JSON.stringify(validatedOutput))}`); - return { success: false }; - } - return { success: true, items: validatedOutput.items }; - } - function getTrackerID(format) { - const trackerID = process.env.GH_AW_TRACKER_ID || ""; - if (trackerID) { - core.info(`Tracker ID: ${trackerID}`); - return format === "markdown" ? `\n\n` : trackerID; - } - return ""; - } - function getMessages() { - const messagesEnv = process.env.GH_AW_SAFE_OUTPUT_MESSAGES; - if (!messagesEnv) { - return null; - } - try { - return JSON.parse(messagesEnv); - } catch (error) { - core.warning(`Failed to parse GH_AW_SAFE_OUTPUT_MESSAGES: ${error instanceof Error ? error.message : String(error)}`); - return null; - } - } - function renderTemplate(template, context) { - return template.replace(/\{(\w+)\}/g, (match, key) => { - const value = context[key]; - return value !== undefined && value !== null ? String(value) : match; - }); - } - function toSnakeCase(obj) { - const result = {}; - for (const [key, value] of Object.entries(obj)) { - const snakeKey = key.replace(/([A-Z])/g, "_$1").toLowerCase(); - result[snakeKey] = value; - result[key] = value; - } - return result; - } - function getCloseOlderDiscussionMessage(ctx) { - const messages = getMessages(); - const templateContext = toSnakeCase(ctx); - const defaultMessage = `⚓ Avast! This discussion be marked as **outdated** by [{workflow_name}]({run_url}). - 🗺️ A newer treasure map awaits ye at **[Discussion #{new_discussion_number}]({new_discussion_url})**. - Fair winds, matey! 🏴‍☠️`; - return messages?.closeOlderDiscussion - ? renderTemplate(messages.closeOlderDiscussion, templateContext) - : renderTemplate(defaultMessage, templateContext); - } - const MAX_CLOSE_COUNT = 10; - const GRAPHQL_DELAY_MS = 500; - function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - async function searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, excludeNumber) { - let searchQuery = `repo:${owner}/${repo} is:open`; - if (titlePrefix) { - const escapedPrefix = titlePrefix.replace(/"/g, '\\"'); - searchQuery += ` in:title "${escapedPrefix}"`; - } - if (labels && labels.length > 0) { - for (const label of labels) { - const escapedLabel = label.replace(/"/g, '\\"'); - searchQuery += ` label:"${escapedLabel}"`; - } - } - const result = await github.graphql( - ` - query($searchTerms: String!, $first: Int!) { - search(query: $searchTerms, type: DISCUSSION, first: $first) { - nodes { - ... on Discussion { - id - number - title - url - category { - id - } - labels(first: 100) { - nodes { - name - } - } - closed - } - } - } - }`, - { searchTerms: searchQuery, first: 50 } - ); - if (!result || !result.search || !result.search.nodes) { - return []; - } - return result.search.nodes - .filter( - d => { - if (!d || d.number === excludeNumber || d.closed) { - return false; - } - if (titlePrefix && d.title && !d.title.startsWith(titlePrefix)) { - return false; - } - if (labels && labels.length > 0) { - const discussionLabels = d.labels?.nodes?.map(( l) => l.name) || []; - const hasAllLabels = labels.every(label => discussionLabels.includes(label)); - if (!hasAllLabels) { - return false; - } - } - if (categoryId && (!d.category || d.category.id !== categoryId)) { - return false; - } - return true; - } - ) - .map( - d => ({ - id: d.id, - number: d.number, - title: d.title, - url: d.url, - }) - ); - } - async function addDiscussionComment(github, discussionId, message) { - const result = await github.graphql( - ` - mutation($dId: ID!, $body: String!) { - addDiscussionComment(input: { discussionId: $dId, body: $body }) { - comment { - id - url - } - } - }`, - { dId: discussionId, body: message } - ); - return result.addDiscussionComment.comment; - } - async function closeDiscussionAsOutdated(github, discussionId) { - const result = await github.graphql( - ` - mutation($dId: ID!) { - closeDiscussion(input: { discussionId: $dId, reason: OUTDATED }) { - discussion { - id - url - } - } - }`, - { dId: discussionId } - ); - return result.closeDiscussion.discussion; - } - async function closeOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion, workflowName, runUrl) { - const searchCriteria = []; - if (titlePrefix) searchCriteria.push(`title prefix: "${titlePrefix}"`); - if (labels && labels.length > 0) searchCriteria.push(`labels: [${labels.join(", ")}]`); - core.info(`Searching for older discussions with ${searchCriteria.join(" and ")}`); - const olderDiscussions = await searchOlderDiscussions(github, owner, repo, titlePrefix, labels, categoryId, newDiscussion.number); - if (olderDiscussions.length === 0) { - core.info("No older discussions found to close"); - return []; - } - core.info(`Found ${olderDiscussions.length} older discussion(s) to close`); - const discussionsToClose = olderDiscussions.slice(0, MAX_CLOSE_COUNT); - if (olderDiscussions.length > MAX_CLOSE_COUNT) { - core.warning(`Found ${olderDiscussions.length} older discussions, but only closing the first ${MAX_CLOSE_COUNT}`); - } - const closedDiscussions = []; - for (let i = 0; i < discussionsToClose.length; i++) { - const discussion = discussionsToClose[i]; - try { - const closingMessage = getCloseOlderDiscussionMessage({ - newDiscussionUrl: newDiscussion.url, - newDiscussionNumber: newDiscussion.number, - workflowName, - runUrl, - }); - core.info(`Adding closing comment to discussion #${discussion.number}`); - await addDiscussionComment(github, discussion.id, closingMessage); - core.info(`Closing discussion #${discussion.number} as outdated`); - await closeDiscussionAsOutdated(github, discussion.id); - closedDiscussions.push({ - number: discussion.number, - url: discussion.url, - }); - core.info(`✓ Closed discussion #${discussion.number}: ${discussion.url}`); - } catch (error) { - core.error(`✗ Failed to close discussion #${discussion.number}: ${error instanceof Error ? error.message : String(error)}`); - } - if (i < discussionsToClose.length - 1) { - await delay(GRAPHQL_DELAY_MS); - } - } - return closedDiscussions; - } - const TEMPORARY_ID_PATTERN = /#(aw_[0-9a-f]{12})/gi; - function generateTemporaryId() { - return "aw_" + crypto.randomBytes(6).toString("hex"); - } - function isTemporaryId(value) { - if (typeof value === "string") { - return /^aw_[0-9a-f]{12}$/i.test(value); - } - return false; - } - function normalizeTemporaryId(tempId) { - return String(tempId).toLowerCase(); - } - function replaceTemporaryIdReferences(text, tempIdMap, currentRepo) { - return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { - const resolved = tempIdMap.get(normalizeTemporaryId(tempId)); - if (resolved !== undefined) { - if (currentRepo && resolved.repo === currentRepo) { - return `#${resolved.number}`; - } - return `${resolved.repo}#${resolved.number}`; - } - return match; - }); - } - function replaceTemporaryIdReferencesLegacy(text, tempIdMap) { - return text.replace(TEMPORARY_ID_PATTERN, (match, tempId) => { - const issueNumber = tempIdMap.get(normalizeTemporaryId(tempId)); - if (issueNumber !== undefined) { - return `#${issueNumber}`; - } - return match; - }); - } - function loadTemporaryIdMap() { - const mapJson = process.env.GH_AW_TEMPORARY_ID_MAP; - if (!mapJson || mapJson === "{}") { - return new Map(); - } - try { - const mapObject = JSON.parse(mapJson); - const result = new Map(); - for (const [key, value] of Object.entries(mapObject)) { - const normalizedKey = normalizeTemporaryId(key); - if (typeof value === "number") { - const contextRepo = `${context.repo.owner}/${context.repo.repo}`; - result.set(normalizedKey, { repo: contextRepo, number: value }); - } else if (typeof value === "object" && value !== null && "repo" in value && "number" in value) { - result.set(normalizedKey, { repo: String(value.repo), number: Number(value.number) }); - } - } - return result; - } catch (error) { - if (typeof core !== "undefined") { - core.warning(`Failed to parse temporary ID map: ${error instanceof Error ? error.message : String(error)}`); - } - return new Map(); - } - } - function resolveIssueNumber(value, temporaryIdMap) { - if (value === undefined || value === null) { - return { resolved: null, wasTemporaryId: false, errorMessage: "Issue number is missing" }; - } - const valueStr = String(value); - if (isTemporaryId(valueStr)) { - const resolvedPair = temporaryIdMap.get(normalizeTemporaryId(valueStr)); - if (resolvedPair !== undefined) { - return { resolved: resolvedPair, wasTemporaryId: true, errorMessage: null }; - } - return { - resolved: null, - wasTemporaryId: true, - errorMessage: `Temporary ID '${valueStr}' not found in map. Ensure the issue was created before linking.`, - }; - } - const issueNumber = typeof value === "number" ? value : parseInt(valueStr, 10); - if (isNaN(issueNumber) || issueNumber <= 0) { - return { resolved: null, wasTemporaryId: false, errorMessage: `Invalid issue number: ${value}` }; - } - const contextRepo = typeof context !== "undefined" ? `${context.repo.owner}/${context.repo.repo}` : ""; - return { resolved: { repo: contextRepo, number: issueNumber }, wasTemporaryId: false, errorMessage: null }; - } - function serializeTemporaryIdMap(tempIdMap) { - const obj = Object.fromEntries(tempIdMap); - return JSON.stringify(obj); - } - function parseAllowedRepos() { - const allowedReposEnv = process.env.GH_AW_ALLOWED_REPOS; - const set = new Set(); - if (allowedReposEnv) { - allowedReposEnv - .split(",") - .map(repo => repo.trim()) - .filter(repo => repo) - .forEach(repo => set.add(repo)); - } - return set; - } - function getDefaultTargetRepo() { - const targetRepoSlug = process.env.GH_AW_TARGET_REPO_SLUG; - if (targetRepoSlug) { - return targetRepoSlug; - } - return `${context.repo.owner}/${context.repo.repo}`; - } - function validateRepo(repo, defaultRepo, allowedRepos) { - if (repo === defaultRepo) { - return { valid: true, error: null }; - } - if (allowedRepos.has(repo)) { - return { valid: true, error: null }; - } - return { - valid: false, - error: `Repository '${repo}' is not in the allowed-repos list. Allowed: ${defaultRepo}${allowedRepos.size > 0 ? ", " + Array.from(allowedRepos).join(", ") : ""}`, - }; - } - function parseRepoSlug(repoSlug) { - const parts = repoSlug.split("/"); - if (parts.length !== 2 || !parts[0] || !parts[1]) { - return null; - } - return { owner: parts[0], repo: parts[1] }; - } - function addExpirationComment(bodyLines, envVarName, entityType) { - const expiresEnv = process.env[envVarName]; - if (expiresEnv) { - const expiresDays = parseInt(expiresEnv, 10); - if (!isNaN(expiresDays) && expiresDays > 0) { - const expirationDate = new Date(); - expirationDate.setDate(expirationDate.getDate() + expiresDays); - const expirationISO = expirationDate.toISOString(); - bodyLines.push(``); - core.info(`${entityType} will expire on ${expirationISO} (${expiresDays} days)`); - } - } - } + const { loadAgentOutput } = require("./load_agent_output.cjs"); + const { getTrackerID } = require("./get_tracker_id.cjs"); + const { closeOlderDiscussions } = require("./close_older_discussions.cjs"); + const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs"); + const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs"); + const { addExpirationComment } = require("./expiration_helpers.cjs"); + const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6564,7 +6196,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/daily-workflow-updater.lock.yml b/.github/workflows/daily-workflow-updater.lock.yml index 1779f386a68..e3877efde59 100644 --- a/.github/workflows/daily-workflow-updater.lock.yml +++ b/.github/workflows/daily-workflow-updater.lock.yml @@ -6330,6 +6330,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6521,7 +6540,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/deep-report.lock.yml b/.github/workflows/deep-report.lock.yml index 7c38e7396b9..42bd9259460 100644 --- a/.github/workflows/deep-report.lock.yml +++ b/.github/workflows/deep-report.lock.yml @@ -6851,6 +6851,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7035,7 +7054,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index 3fdbd27f80a..a2b68bf0324 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -7112,6 +7112,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7244,6 +7263,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/dev.lock.yml b/.github/workflows/dev.lock.yml index 79d74ce7307..c759495732b 100644 --- a/.github/workflows/dev.lock.yml +++ b/.github/workflows/dev.lock.yml @@ -7113,6 +7113,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7304,7 +7323,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/developer-docs-consolidator.lock.yml b/.github/workflows/developer-docs-consolidator.lock.yml index b1a358a4577..5b3d241d6dc 100644 --- a/.github/workflows/developer-docs-consolidator.lock.yml +++ b/.github/workflows/developer-docs-consolidator.lock.yml @@ -7266,6 +7266,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7450,7 +7469,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } @@ -7751,6 +7772,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7942,7 +7982,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/dictation-prompt.lock.yml b/.github/workflows/dictation-prompt.lock.yml index b159fe89602..e2dd58c44f9 100644 --- a/.github/workflows/dictation-prompt.lock.yml +++ b/.github/workflows/dictation-prompt.lock.yml @@ -6273,6 +6273,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6464,7 +6483,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index f119c047511..67483ef060c 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -6626,6 +6626,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6810,7 +6829,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/duplicate-code-detector.lock.yml b/.github/workflows/duplicate-code-detector.lock.yml index f274b35458a..7e157c27d18 100644 --- a/.github/workflows/duplicate-code-detector.lock.yml +++ b/.github/workflows/duplicate-code-detector.lock.yml @@ -6088,6 +6088,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6220,6 +6239,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/example-workflow-analyzer.lock.yml b/.github/workflows/example-workflow-analyzer.lock.yml index 563ec3d04c1..929c613e292 100644 --- a/.github/workflows/example-workflow-analyzer.lock.yml +++ b/.github/workflows/example-workflow-analyzer.lock.yml @@ -5812,6 +5812,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -5996,7 +6015,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/github-mcp-structural-analysis.lock.yml b/.github/workflows/github-mcp-structural-analysis.lock.yml index 596b2bd7640..eb0c38af879 100644 --- a/.github/workflows/github-mcp-structural-analysis.lock.yml +++ b/.github/workflows/github-mcp-structural-analysis.lock.yml @@ -7192,6 +7192,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7376,7 +7395,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/github-mcp-tools-report.lock.yml b/.github/workflows/github-mcp-tools-report.lock.yml index 1b044554f97..a20e7ad7905 100644 --- a/.github/workflows/github-mcp-tools-report.lock.yml +++ b/.github/workflows/github-mcp-tools-report.lock.yml @@ -6969,6 +6969,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7153,7 +7172,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } @@ -7454,6 +7475,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7645,7 +7685,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/glossary-maintainer.lock.yml b/.github/workflows/glossary-maintainer.lock.yml index dbb5ebcb054..7e0f75d5079 100644 --- a/.github/workflows/glossary-maintainer.lock.yml +++ b/.github/workflows/glossary-maintainer.lock.yml @@ -7438,6 +7438,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7629,7 +7648,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/go-fan.lock.yml b/.github/workflows/go-fan.lock.yml index 058332bb320..3f5f3880f37 100644 --- a/.github/workflows/go-fan.lock.yml +++ b/.github/workflows/go-fan.lock.yml @@ -6537,6 +6537,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6721,7 +6740,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/go-logger.lock.yml b/.github/workflows/go-logger.lock.yml index 1716e6d6a20..5dd05ac0673 100644 --- a/.github/workflows/go-logger.lock.yml +++ b/.github/workflows/go-logger.lock.yml @@ -6059,6 +6059,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6250,7 +6269,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/go-pattern-detector.lock.yml b/.github/workflows/go-pattern-detector.lock.yml index 8c1e2715c74..9d016fcabbc 100644 --- a/.github/workflows/go-pattern-detector.lock.yml +++ b/.github/workflows/go-pattern-detector.lock.yml @@ -5935,6 +5935,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6067,6 +6086,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/instructions-janitor.lock.yml b/.github/workflows/instructions-janitor.lock.yml index 7bb68e97428..f0c147849f3 100644 --- a/.github/workflows/instructions-janitor.lock.yml +++ b/.github/workflows/instructions-janitor.lock.yml @@ -5824,6 +5824,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6015,7 +6034,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 61ac22d859a..1ccf5a77290 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -6205,6 +6205,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6389,7 +6408,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/layout-spec-maintainer.lock.yml b/.github/workflows/layout-spec-maintainer.lock.yml index 6a320d172c0..f4fdd703742 100644 --- a/.github/workflows/layout-spec-maintainer.lock.yml +++ b/.github/workflows/layout-spec-maintainer.lock.yml @@ -6568,6 +6568,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6759,7 +6778,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/lockfile-stats.lock.yml b/.github/workflows/lockfile-stats.lock.yml index 9b6aa4f79a6..81eab4b3cea 100644 --- a/.github/workflows/lockfile-stats.lock.yml +++ b/.github/workflows/lockfile-stats.lock.yml @@ -6543,6 +6543,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6727,7 +6746,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 14ddab76a75..f17cd98d860 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -7159,6 +7159,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7343,7 +7362,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/org-health-report.lock.yml b/.github/workflows/org-health-report.lock.yml index 1af6e6b9a0b..2c241ce7395 100644 --- a/.github/workflows/org-health-report.lock.yml +++ b/.github/workflows/org-health-report.lock.yml @@ -8024,6 +8024,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8208,7 +8227,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/plan.lock.yml b/.github/workflows/plan.lock.yml index e97f09d8140..e57b333e4ca 100644 --- a/.github/workflows/plan.lock.yml +++ b/.github/workflows/plan.lock.yml @@ -7715,6 +7715,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7847,6 +7866,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/poem-bot.lock.yml b/.github/workflows/poem-bot.lock.yml index f03a17ad34c..8b4378013e7 100644 --- a/.github/workflows/poem-bot.lock.yml +++ b/.github/workflows/poem-bot.lock.yml @@ -9359,6 +9359,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -9543,7 +9562,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } @@ -9952,6 +9973,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -10084,6 +10124,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; @@ -10826,6 +10867,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -11017,7 +11077,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; @@ -12942,6 +13004,25 @@ jobs: core.setFailed(error instanceof Error ? error : String(error)); } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function resolveTargetNumber(params) { const { updateTarget, item, numberField, isValidContext, contextNumber, displayName } = params; if (updateTarget === "*") { @@ -12982,10 +13063,12 @@ jobs: logMessages.push(`Invalid status value: ${item.status}. Must be 'open' or 'closed'`); } } + let titleForDedup = null; if (canUpdateTitle && item.title !== undefined) { const trimmedTitle = typeof item.title === "string" ? item.title.trim() : ""; if (trimmedTitle.length > 0) { updateData.title = trimmedTitle; + titleForDedup = trimmedTitle; hasUpdates = true; logMessages.push(`Will update title to: ${trimmedTitle}`); } else { @@ -12994,9 +13077,13 @@ jobs: } if (canUpdateBody && item.body !== undefined) { if (typeof item.body === "string") { - updateData.body = item.body; + let processedBody = item.body; + if (titleForDedup) { + processedBody = removeDuplicateTitleFromDescription(titleForDedup, processedBody); + } + updateData.body = processedBody; hasUpdates = true; - logMessages.push(`Will update body (length: ${item.body.length})`); + logMessages.push(`Will update body (length: ${processedBody.length})`); } else { logMessages.push("Invalid body value: must be a string"); } diff --git a/.github/workflows/portfolio-analyst.lock.yml b/.github/workflows/portfolio-analyst.lock.yml index d1bdf0d6fde..f874e695a6f 100644 --- a/.github/workflows/portfolio-analyst.lock.yml +++ b/.github/workflows/portfolio-analyst.lock.yml @@ -7397,6 +7397,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7581,7 +7600,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/pr-nitpick-reviewer.lock.yml b/.github/workflows/pr-nitpick-reviewer.lock.yml index 737da27e9d7..4c61a5607ac 100644 --- a/.github/workflows/pr-nitpick-reviewer.lock.yml +++ b/.github/workflows/pr-nitpick-reviewer.lock.yml @@ -8325,6 +8325,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8509,7 +8528,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/prompt-clustering-analysis.lock.yml b/.github/workflows/prompt-clustering-analysis.lock.yml index 117660a7b52..f8ef762bea3 100644 --- a/.github/workflows/prompt-clustering-analysis.lock.yml +++ b/.github/workflows/prompt-clustering-analysis.lock.yml @@ -7821,6 +7821,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8005,7 +8024,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/python-data-charts.lock.yml b/.github/workflows/python-data-charts.lock.yml index e385a5920a9..aabe59f648e 100644 --- a/.github/workflows/python-data-charts.lock.yml +++ b/.github/workflows/python-data-charts.lock.yml @@ -8390,6 +8390,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -8574,7 +8593,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index ff65d7df3d4..c15353ca102 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -8383,6 +8383,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -8574,7 +8593,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/repo-tree-map.lock.yml b/.github/workflows/repo-tree-map.lock.yml index 0abbb82d122..d033398ba4f 100644 --- a/.github/workflows/repo-tree-map.lock.yml +++ b/.github/workflows/repo-tree-map.lock.yml @@ -6553,6 +6553,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6737,7 +6756,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/repository-quality-improver.lock.yml b/.github/workflows/repository-quality-improver.lock.yml index bd0bfac8946..d194cf910c1 100644 --- a/.github/workflows/repository-quality-improver.lock.yml +++ b/.github/workflows/repository-quality-improver.lock.yml @@ -7597,6 +7597,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7781,7 +7800,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/research.lock.yml b/.github/workflows/research.lock.yml index 82410b9779f..be4311f5b18 100644 --- a/.github/workflows/research.lock.yml +++ b/.github/workflows/research.lock.yml @@ -6466,6 +6466,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6650,7 +6669,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/safe-output-health.lock.yml b/.github/workflows/safe-output-health.lock.yml index 498e3a50d04..bddd25b1655 100644 --- a/.github/workflows/safe-output-health.lock.yml +++ b/.github/workflows/safe-output-health.lock.yml @@ -6842,6 +6842,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7026,7 +7045,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index cec7e76392d..bf11ff2504c 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -6489,6 +6489,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6673,7 +6692,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/security-fix-pr.lock.yml b/.github/workflows/security-fix-pr.lock.yml index 268031e6f92..4d299d4e37f 100644 --- a/.github/workflows/security-fix-pr.lock.yml +++ b/.github/workflows/security-fix-pr.lock.yml @@ -5832,6 +5832,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6023,7 +6042,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index cdc5efdfdaf..3f87d2251c9 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -6842,6 +6842,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6974,6 +6993,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index b30ab12be33..f528a1bacf2 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -7669,6 +7669,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7801,6 +7820,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 6bb1d5c53e3..b91e6e389c0 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -7482,6 +7482,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7614,6 +7633,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index 02788a96ed1..f3aae717596 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -9390,6 +9390,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -9522,6 +9541,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; @@ -10211,6 +10231,25 @@ jobs: core.setFailed(error instanceof Error ? error : String(error)); } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function resolveTargetNumber(params) { const { updateTarget, item, numberField, isValidContext, contextNumber, displayName } = params; if (updateTarget === "*") { @@ -10251,10 +10290,12 @@ jobs: logMessages.push(`Invalid status value: ${item.status}. Must be 'open' or 'closed'`); } } + let titleForDedup = null; if (canUpdateTitle && item.title !== undefined) { const trimmedTitle = typeof item.title === "string" ? item.title.trim() : ""; if (trimmedTitle.length > 0) { updateData.title = trimmedTitle; + titleForDedup = trimmedTitle; hasUpdates = true; logMessages.push(`Will update title to: ${trimmedTitle}`); } else { @@ -10263,9 +10304,13 @@ jobs: } if (canUpdateBody && item.body !== undefined) { if (typeof item.body === "string") { - updateData.body = item.body; + let processedBody = item.body; + if (titleForDedup) { + processedBody = removeDuplicateTitleFromDescription(titleForDedup, processedBody); + } + updateData.body = processedBody; hasUpdates = true; - logMessages.push(`Will update body (length: ${item.body.length})`); + logMessages.push(`Will update body (length: ${processedBody.length})`); } else { logMessages.push("Invalid body value: must be a string"); } diff --git a/.github/workflows/smoke-copilot-playwright.lock.yml b/.github/workflows/smoke-copilot-playwright.lock.yml index 3b5a5a8d0b9..f48f85249a1 100644 --- a/.github/workflows/smoke-copilot-playwright.lock.yml +++ b/.github/workflows/smoke-copilot-playwright.lock.yml @@ -9381,6 +9381,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -9513,6 +9532,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-copilot-safe-inputs.lock.yml b/.github/workflows/smoke-copilot-safe-inputs.lock.yml index cda04e80f41..26c4d3ed2e6 100644 --- a/.github/workflows/smoke-copilot-safe-inputs.lock.yml +++ b/.github/workflows/smoke-copilot-safe-inputs.lock.yml @@ -9087,6 +9087,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -9219,6 +9238,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 971c3b71cf7..3c6bd7c34c4 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -7912,6 +7912,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -8044,6 +8063,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/smoke-detector.lock.yml b/.github/workflows/smoke-detector.lock.yml index 46ab4cad34a..ff5e1efba18 100644 --- a/.github/workflows/smoke-detector.lock.yml +++ b/.github/workflows/smoke-detector.lock.yml @@ -7415,6 +7415,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7547,6 +7566,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/spec-kit-execute.lock.yml b/.github/workflows/spec-kit-execute.lock.yml index f44f9f644a1..b30a35de64a 100644 --- a/.github/workflows/spec-kit-execute.lock.yml +++ b/.github/workflows/spec-kit-execute.lock.yml @@ -6896,6 +6896,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7087,7 +7106,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/spec-kit-executor.lock.yml b/.github/workflows/spec-kit-executor.lock.yml index 84625d7123a..1aa8b03cf76 100644 --- a/.github/workflows/spec-kit-executor.lock.yml +++ b/.github/workflows/spec-kit-executor.lock.yml @@ -6586,6 +6586,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6777,7 +6796,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/speckit-dispatcher.lock.yml b/.github/workflows/speckit-dispatcher.lock.yml index 9f10f8de835..f403fc9e3d6 100644 --- a/.github/workflows/speckit-dispatcher.lock.yml +++ b/.github/workflows/speckit-dispatcher.lock.yml @@ -8382,6 +8382,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -8514,6 +8533,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 73a5310138a..51e3291f9e3 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -8154,6 +8154,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -8286,6 +8305,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/static-analysis-report.lock.yml b/.github/workflows/static-analysis-report.lock.yml index 104dc36b85c..7050b27b3d5 100644 --- a/.github/workflows/static-analysis-report.lock.yml +++ b/.github/workflows/static-analysis-report.lock.yml @@ -6581,6 +6581,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6765,7 +6784,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/super-linter.lock.yml b/.github/workflows/super-linter.lock.yml index 015ee971493..c1eca98e6df 100644 --- a/.github/workflows/super-linter.lock.yml +++ b/.github/workflows/super-linter.lock.yml @@ -6667,6 +6667,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6799,6 +6818,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/technical-doc-writer.lock.yml b/.github/workflows/technical-doc-writer.lock.yml index d6b4d830bdd..e6754cc55a6 100644 --- a/.github/workflows/technical-doc-writer.lock.yml +++ b/.github/workflows/technical-doc-writer.lock.yml @@ -7638,6 +7638,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7829,7 +7848,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/test-discussion-expires.lock.yml b/.github/workflows/test-discussion-expires.lock.yml index a7605fc0daa..b230abe655a 100644 --- a/.github/workflows/test-discussion-expires.lock.yml +++ b/.github/workflows/test-discussion-expires.lock.yml @@ -6153,6 +6153,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -6337,7 +6356,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/test-python-safe-input.lock.yml b/.github/workflows/test-python-safe-input.lock.yml index 6767705135b..dfc1751e8e8 100644 --- a/.github/workflows/test-python-safe-input.lock.yml +++ b/.github/workflows/test-python-safe-input.lock.yml @@ -7662,6 +7662,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -7794,6 +7813,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/tidy.lock.yml b/.github/workflows/tidy.lock.yml index 7235fd0e16a..56a7abc9453 100644 --- a/.github/workflows/tidy.lock.yml +++ b/.github/workflows/tidy.lock.yml @@ -6683,6 +6683,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -6874,7 +6893,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/typist.lock.yml b/.github/workflows/typist.lock.yml index fef8aef74f4..e3d01d28c0a 100644 --- a/.github/workflows/typist.lock.yml +++ b/.github/workflows/typist.lock.yml @@ -6899,6 +6899,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7083,7 +7102,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 13a1a55839e..84f7bcc45e4 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -7447,6 +7447,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } function generatePatchPreview(patchContent) { if (!patchContent || !patchContent.trim()) { return ""; @@ -7638,7 +7657,9 @@ jobs: return; } let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; if (!title) { title = "Agent Output"; diff --git a/.github/workflows/video-analyzer.lock.yml b/.github/workflows/video-analyzer.lock.yml index 827e1dd03a1..42b49729dbe 100644 --- a/.github/workflows/video-analyzer.lock.yml +++ b/.github/workflows/video-analyzer.lock.yml @@ -6701,6 +6701,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function main() { core.setOutput("issue_number", ""); core.setOutput("issue_url", ""); @@ -6833,6 +6852,7 @@ jobs: .filter((label, index, arr) => arr.indexOf(label) === index); let title = createIssueItem.title ? createIssueItem.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); let bodyLines = processedBody.split("\n"); if (!title) { title = createIssueItem.body || "Agent Output"; diff --git a/.github/workflows/weekly-issue-summary.lock.yml b/.github/workflows/weekly-issue-summary.lock.yml index 67ebf5f3b6c..ed24f249730 100644 --- a/.github/workflows/weekly-issue-summary.lock.yml +++ b/.github/workflows/weekly-issue-summary.lock.yml @@ -7621,6 +7621,25 @@ jobs: } } } + function removeDuplicateTitleFromDescription(title, description) { + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + return trimmedDescription; + } async function fetchRepoDiscussionInfo(owner, repo) { const repositoryQuery = ` query($owner: String!, $repo: String!) { @@ -7805,7 +7824,9 @@ jobs: ); let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/pkg/workflow/js.go b/pkg/workflow/js.go index d680d9b2d06..f1971a9b449 100644 --- a/pkg/workflow/js.go +++ b/pkg/workflow/js.go @@ -246,6 +246,9 @@ var safeOutputTypeValidatorScript string //go:embed js/repo_helpers.cjs var repoHelpersScript string +//go:embed js/remove_duplicate_title.cjs +var removeDuplicateTitleScript string + //go:embed js/safe_outputs_config.cjs var safeOutputsConfigScript string @@ -314,6 +317,7 @@ func GetJavaScriptSources() map[string]string { "mcp_handler_python.cjs": mcpHandlerPythonScript, "safe_output_type_validator.cjs": safeOutputTypeValidatorScript, "repo_helpers.cjs": repoHelpersScript, + "remove_duplicate_title.cjs": removeDuplicateTitleScript, "safe_outputs_config.cjs": safeOutputsConfigScript, "safe_outputs_append.cjs": safeOutputsAppendScript, "safe_outputs_handlers.cjs": safeOutputsHandlersScript, diff --git a/pkg/workflow/js/create_discussion.cjs b/pkg/workflow/js/create_discussion.cjs index 14cc337d1ce..204d7ee54e6 100644 --- a/pkg/workflow/js/create_discussion.cjs +++ b/pkg/workflow/js/create_discussion.cjs @@ -7,6 +7,7 @@ const { closeOlderDiscussions } = require("./close_older_discussions.cjs"); const { replaceTemporaryIdReferences, loadTemporaryIdMap } = require("./temporary_id.cjs"); const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs"); const { addExpirationComment } = require("./expiration_helpers.cjs"); +const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); /** * Fetch repository ID and discussion categories for a repository @@ -247,7 +248,12 @@ async function main() { let title = createDiscussionItem.title ? replaceTemporaryIdReferences(createDiscussionItem.title.trim(), temporaryIdMap, itemRepo) : ""; // Replace temporary ID references in body (with defensive null check) const bodyText = createDiscussionItem.body || ""; - let bodyLines = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo).split("\n"); + let processedBody = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo); + + // Remove duplicate title from description if it starts with a header matching the title + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + + let bodyLines = processedBody.split("\n"); if (!title) { title = replaceTemporaryIdReferences(bodyText, temporaryIdMap, itemRepo) || "Agent Output"; } diff --git a/pkg/workflow/js/create_issue.cjs b/pkg/workflow/js/create_issue.cjs index 573dcdb5582..91956c93ea9 100644 --- a/pkg/workflow/js/create_issue.cjs +++ b/pkg/workflow/js/create_issue.cjs @@ -15,6 +15,7 @@ const { } = require("./temporary_id.cjs"); const { parseAllowedRepos, getDefaultTargetRepo, validateRepo, parseRepoSlug } = require("./repo_helpers.cjs"); const { addExpirationComment } = require("./expiration_helpers.cjs"); +const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); async function main() { // Initialize outputs to empty strings to ensure they're always set @@ -179,6 +180,10 @@ async function main() { // Replace temporary ID references in the body using already-created issues let processedBody = replaceTemporaryIdReferences(createIssueItem.body, temporaryIdMap, itemRepo); + + // Remove duplicate title from description if it starts with a header matching the title + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + let bodyLines = processedBody.split("\n"); if (!title) { diff --git a/pkg/workflow/js/create_pull_request.cjs b/pkg/workflow/js/create_pull_request.cjs index c4d2f286d96..9e75228b359 100644 --- a/pkg/workflow/js/create_pull_request.cjs +++ b/pkg/workflow/js/create_pull_request.cjs @@ -8,6 +8,7 @@ const crypto = require("crypto"); const { updateActivationComment } = require("./update_activation_comment.cjs"); const { getTrackerID } = require("./get_tracker_id.cjs"); const { addExpirationComment } = require("./expiration_helpers.cjs"); +const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); /** * Generate a patch preview with max 500 lines and 2000 chars for issue body @@ -274,7 +275,12 @@ async function main() { // Extract title, body, and branch from the JSON item let title = pullRequestItem.title.trim(); - let bodyLines = pullRequestItem.body.split("\n"); + let processedBody = pullRequestItem.body; + + // Remove duplicate title from description if it starts with a header matching the title + processedBody = removeDuplicateTitleFromDescription(title, processedBody); + + let bodyLines = processedBody.split("\n"); let branchName = pullRequestItem.branch ? pullRequestItem.branch.trim() : null; // If no title was found, use a default diff --git a/pkg/workflow/js/create_pull_request.test.cjs b/pkg/workflow/js/create_pull_request.test.cjs index e22bad69031..dbafbc330af 100644 --- a/pkg/workflow/js/create_pull_request.test.cjs +++ b/pkg/workflow/js/create_pull_request.test.cjs @@ -18,10 +18,14 @@ const createTestableFunction = scriptContent => { scriptBody = scriptBody.replace(/const \{ updateActivationComment \} = require\("\.\/update_activation_comment\.cjs"\);?\s*/g, ""); scriptBody = scriptBody.replace(/const \{ getTrackerID \} = require\("\.\/get_tracker_id\.cjs"\);?\s*/g, ""); scriptBody = scriptBody.replace(/const \{ addExpirationComment \} = require\("\.\/expiration_helpers\.cjs"\);?\s*/g, ""); + scriptBody = scriptBody.replace( + /const \{ removeDuplicateTitleFromDescription \} = require\("\.\/remove_duplicate_title\.cjs"\);?\s*/g, + "" + ); // Create a testable function that has the same logic but can be called with dependencies return new Function(` - const { fs, crypto, github, core, context, process, console, updateActivationComment, getTrackerID, addExpirationComment } = arguments[0]; + const { fs, crypto, github, core, context, process, console, updateActivationComment, getTrackerID, addExpirationComment, removeDuplicateTitleFromDescription } = arguments[0]; ${scriptBody} @@ -165,6 +169,7 @@ describe("create_pull_request.cjs", () => { return ""; }), addExpirationComment: vi.fn(), + removeDuplicateTitleFromDescription: vi.fn((title, description) => description), }; }); diff --git a/pkg/workflow/js/remove_duplicate_title.cjs b/pkg/workflow/js/remove_duplicate_title.cjs new file mode 100644 index 00000000000..b4041667ee2 --- /dev/null +++ b/pkg/workflow/js/remove_duplicate_title.cjs @@ -0,0 +1,50 @@ +// @ts-check +/** + * Remove duplicate title from description + * @module remove_duplicate_title + */ + +/** + * Removes duplicate title from the beginning of description content. + * If the description starts with a header (# or ## or ### etc.) that matches + * the title, it will be removed along with any trailing newlines. + * + * @param {string} title - The title text to match and remove + * @param {string} description - The description content that may contain duplicate title + * @returns {string} The description with duplicate title removed + */ +function removeDuplicateTitleFromDescription(title, description) { + // Handle null/undefined/empty inputs + if (!title || typeof title !== "string") { + return description || ""; + } + if (!description || typeof description !== "string") { + return ""; + } + + const trimmedTitle = title.trim(); + const trimmedDescription = description.trim(); + + if (!trimmedTitle || !trimmedDescription) { + return trimmedDescription; + } + + // Match any header level (# to ######) followed by the title at the start + // This regex matches: + // - Start of string + // - One or more # characters + // - One or more spaces + // - The exact title (escaped for regex special chars) + // - Optional trailing spaces + // - Optional newlines after the header + const escapedTitle = trimmedTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const headerRegex = new RegExp(`^#{1,6}\\s+${escapedTitle}\\s*(?:\\r?\\n)*`, "i"); + + if (headerRegex.test(trimmedDescription)) { + return trimmedDescription.replace(headerRegex, "").trim(); + } + + return trimmedDescription; +} + +module.exports = { removeDuplicateTitleFromDescription }; diff --git a/pkg/workflow/js/remove_duplicate_title.test.cjs b/pkg/workflow/js/remove_duplicate_title.test.cjs new file mode 100644 index 00000000000..ac38eb947a8 --- /dev/null +++ b/pkg/workflow/js/remove_duplicate_title.test.cjs @@ -0,0 +1,363 @@ +import { describe, it, expect } from "vitest"; + +// Import the function to test +const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); + +describe("remove_duplicate_title.cjs", () => { + describe("removeDuplicateTitleFromDescription", () => { + // Basic functionality tests + describe("basic functionality", () => { + it("should remove H1 header matching title", () => { + const title = "Bug Report"; + const description = "# Bug Report\n\nThis is the body of the report."; + const expected = "This is the body of the report."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should remove H2 header matching title", () => { + const title = "Feature Request"; + const description = "## Feature Request\n\nThis is the feature description."; + const expected = "This is the feature description."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should remove H3 header matching title", () => { + const title = "Documentation Update"; + const description = "### Documentation Update\n\nThis is the documentation."; + const expected = "This is the documentation."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should remove H4 header matching title", () => { + const title = "Refactoring"; + const description = "#### Refactoring\n\nThis is the refactoring plan."; + const expected = "This is the refactoring plan."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should remove H5 header matching title", () => { + const title = "Test"; + const description = "##### Test\n\nThis is the test description."; + const expected = "This is the test description."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should remove H6 header matching title", () => { + const title = "Note"; + const description = "###### Note\n\nThis is the note content."; + const expected = "This is the note content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Case insensitivity tests + describe("case insensitivity", () => { + it("should match title case-insensitively", () => { + const title = "Bug Report"; + const description = "# bug report\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should match title with different casing", () => { + const title = "BUG REPORT"; + const description = "# Bug Report\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should match lowercase title", () => { + const title = "feature request"; + const description = "# Feature Request\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Whitespace handling tests + describe("whitespace handling", () => { + it("should handle extra spaces after hash", () => { + const title = "Title"; + const description = "# Title\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle trailing spaces after title", () => { + const title = "Title"; + const description = "# Title \n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle multiple newlines after header", () => { + const title = "Title"; + const description = "# Title\n\n\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle CRLF line endings", () => { + const title = "Title"; + const description = "# Title\r\n\r\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should trim leading/trailing whitespace from inputs", () => { + const title = " Title "; + const description = " # Title\n\nBody content. "; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Non-matching cases + describe("non-matching cases", () => { + it("should not remove header when title doesn't match", () => { + const title = "Bug Report"; + const description = "# Feature Request\n\nBody content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(description); + }); + + it("should not remove header when it's not at the start", () => { + const title = "Title"; + const description = "Some text\n\n# Title\n\nBody content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(description); + }); + + it("should not remove title if it's not in a header", () => { + const title = "Title"; + const description = "Title\n\nBody content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(description); + }); + + it("should preserve description when no header matches", () => { + const title = "Title"; + const description = "This is just body content without headers."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(description); + }); + }); + + // Special characters in title + describe("special characters in title", () => { + it("should handle title with parentheses", () => { + const title = "Bug Report (Important)"; + const description = "# Bug Report (Important)\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with brackets", () => { + const title = "Feature [v2.0]"; + const description = "# Feature [v2.0]\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with dots and asterisks", () => { + const title = "Fix *.txt files"; + const description = "# Fix *.txt files\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with plus and question marks", () => { + const title = "C++ Update?"; + const description = "# C++ Update?\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with dollar signs", () => { + const title = "Fix $VAR usage"; + const description = "# Fix $VAR usage\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with carets and pipes", () => { + const title = "Test ^pattern|filter"; + const description = "# Test ^pattern|filter\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with curly braces", () => { + const title = "Fix {key: value}"; + const description = "# Fix {key: value}\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with backslashes", () => { + const title = "Path\\to\\file"; + const description = "# Path\\to\\file\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Edge cases + describe("edge cases", () => { + it("should return empty string when both inputs are empty", () => { + expect(removeDuplicateTitleFromDescription("", "")).toBe(""); + }); + + it("should return empty string when description is empty", () => { + expect(removeDuplicateTitleFromDescription("Title", "")).toBe(""); + }); + + it("should return description when title is empty", () => { + const description = "# Some Header\n\nBody content."; + expect(removeDuplicateTitleFromDescription("", description)).toBe(description); + }); + + it("should handle null title", () => { + const description = "# Title\n\nBody content."; + expect(removeDuplicateTitleFromDescription(null, description)).toBe(description); + }); + + it("should handle undefined title", () => { + const description = "# Title\n\nBody content."; + expect(removeDuplicateTitleFromDescription(undefined, description)).toBe(description); + }); + + it("should handle null description", () => { + expect(removeDuplicateTitleFromDescription("Title", null)).toBe(""); + }); + + it("should handle undefined description", () => { + expect(removeDuplicateTitleFromDescription("Title", undefined)).toBe(""); + }); + + it("should handle non-string title", () => { + const description = "# 123\n\nBody content."; + expect(removeDuplicateTitleFromDescription(123, description)).toBe(description); + }); + + it("should handle non-string description", () => { + expect(removeDuplicateTitleFromDescription("Title", 123)).toBe(""); + }); + }); + + // Complex scenarios + describe("complex scenarios", () => { + it("should handle description with only the header", () => { + const title = "Title"; + const description = "# Title"; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(""); + }); + + it("should handle description with header and no content", () => { + const title = "Title"; + const description = "# Title\n\n"; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(""); + }); + + it("should preserve other headers in the description", () => { + const title = "Main Title"; + const description = "# Main Title\n\n## Section 1\n\nContent here.\n\n## Section 2\n\nMore content."; + const expected = "## Section 1\n\nContent here.\n\n## Section 2\n\nMore content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle description with multiple paragraphs", () => { + const title = "Bug Report"; + const description = "# Bug Report\n\nFirst paragraph.\n\nSecond paragraph.\n\nThird paragraph."; + const expected = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle description with code blocks", () => { + const title = "Code Fix"; + const description = "# Code Fix\n\n```js\nconst x = 1;\n```\n\nExplanation."; + const expected = "```js\nconst x = 1;\n```\n\nExplanation."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle description with lists", () => { + const title = "Tasks"; + const description = "# Tasks\n\n- Task 1\n- Task 2\n- Task 3"; + const expected = "- Task 1\n- Task 2\n- Task 3"; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle title with numbers", () => { + const title = "Version 2.0 Release"; + const description = "# Version 2.0 Release\n\nRelease notes here."; + const expected = "Release notes here."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle very long titles", () => { + const title = "This is a very long title that contains many words and should still be matched correctly"; + const description = `# ${title}\n\nBody content.`; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle emoji in title", () => { + const title = "🐛 Bug Report"; + const description = "# 🐛 Bug Report\n\nBody content."; + const expected = "Body content."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle unicode characters in title", () => { + const title = "Прошу исправить ошибку"; + const description = "# Прошу исправить ошибку\n\nОписание проблемы."; + const expected = "Описание проблемы."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Real-world examples + describe("real-world examples", () => { + it("should handle GitHub issue format", () => { + const title = "Feature: Add dark mode"; + const description = + "# Feature: Add dark mode\n\n## Description\n\nWe need dark mode support.\n\n## Acceptance Criteria\n\n- [ ] Dark mode toggle\n- [ ] Persistent preference"; + const expected = + "## Description\n\nWe need dark mode support.\n\n## Acceptance Criteria\n\n- [ ] Dark mode toggle\n- [ ] Persistent preference"; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle pull request format", () => { + const title = "Fix authentication bug"; + const description = + "# Fix authentication bug\n\n## Changes\n\n- Updated auth flow\n- Added tests\n\n## Testing\n\nManually tested all scenarios."; + const expected = "## Changes\n\n- Updated auth flow\n- Added tests\n\n## Testing\n\nManually tested all scenarios."; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + + it("should handle discussion format", () => { + const title = "How to configure X?"; + const description = "# How to configure X?\n\nI'm trying to configure X but can't find the documentation.\n\nCan someone help?"; + const expected = "I'm trying to configure X but can't find the documentation.\n\nCan someone help?"; + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + }); + }); + + // Performance considerations + describe("performance", () => { + it("should handle large descriptions efficiently", () => { + const title = "Title"; + const largeBody = "Body content.\n".repeat(1000); + const description = `# Title\n\n${largeBody}`; + const result = removeDuplicateTitleFromDescription(title, description); + expect(result).toBe(largeBody.trim()); + }); + + it("should handle multiple consecutive calls", () => { + const title = "Title"; + const description = "# Title\n\nBody content."; + const expected = "Body content."; + + for (let i = 0; i < 100; i++) { + expect(removeDuplicateTitleFromDescription(title, description)).toBe(expected); + } + }); + }); + }); +}); diff --git a/pkg/workflow/js/update_runner.cjs b/pkg/workflow/js/update_runner.cjs index 974bf475349..b3737dc0a9f 100644 --- a/pkg/workflow/js/update_runner.cjs +++ b/pkg/workflow/js/update_runner.cjs @@ -14,6 +14,7 @@ const { loadAgentOutput } = require("./load_agent_output.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); +const { removeDuplicateTitleFromDescription } = require("./remove_duplicate_title.cjs"); /** * @typedef {Object} UpdateRunnerConfig @@ -104,10 +105,12 @@ function buildUpdateData(params) { } // Handle title update + let titleForDedup = null; if (canUpdateTitle && item.title !== undefined) { const trimmedTitle = typeof item.title === "string" ? item.title.trim() : ""; if (trimmedTitle.length > 0) { updateData.title = trimmedTitle; + titleForDedup = trimmedTitle; hasUpdates = true; logMessages.push(`Will update title to: ${trimmedTitle}`); } else { @@ -115,12 +118,19 @@ function buildUpdateData(params) { } } - // Handle body update (basic - without operation logic) + // Handle body update (with title deduplication) if (canUpdateBody && item.body !== undefined) { if (typeof item.body === "string") { - updateData.body = item.body; + let processedBody = item.body; + + // If we're updating the title at the same time, remove duplicate title from body + if (titleForDedup) { + processedBody = removeDuplicateTitleFromDescription(titleForDedup, processedBody); + } + + updateData.body = processedBody; hasUpdates = true; - logMessages.push(`Will update body (length: ${item.body.length})`); + logMessages.push(`Will update body (length: ${processedBody.length})`); } else { logMessages.push("Invalid body value: must be a string"); }