From b8a4078be7e58b0ee404d28809cca028b70aedb5 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Thu, 11 Dec 2025 21:46:42 +0800 Subject: [PATCH 1/4] feat: add `syncCommentWithIssue` option --- packages/app/server/routes/publish.post.ts | 132 ++++++++++++++++----- packages/cli/index.ts | 8 ++ 2 files changed, 113 insertions(+), 27 deletions(-) diff --git a/packages/app/server/routes/publish.post.ts b/packages/app/server/routes/publish.post.ts index d8d08ba7..59b5c132 100644 --- a/packages/app/server/routes/publish.post.ts +++ b/packages/app/server/routes/publish.post.ts @@ -23,6 +23,7 @@ export default eventHandler(async (event) => { "sb-only-templates": onlyTemplatesHeader, "sb-comment-with-sha": commentWithShaHeader, "sb-comment-with-dev": commentWithDevHeader, + "sb-sync-comment-with-issue": syncCommentWithIssueHeader, } = getHeaders(event); const compact = compactHeader === "true"; const onlyTemplates = onlyTemplatesHeader === "true"; @@ -32,6 +33,7 @@ export default eventHandler(async (event) => { (packageManagerHeader as PackageManager) || "npm"; const commentWithSha = commentWithShaHeader === "true"; const commentWithDev = commentWithDevHeader === "true"; + const syncCommentWithIssue = syncCommentWithIssueHeader === "true"; if (!key || !runIdHeader || !shasumsHeader) { throw createError({ @@ -246,7 +248,14 @@ export default eventHandler(async (event) => { isPullRequest(workflowData.ref) && (await getPullRequestState(installation, workflowData)) === "open" ) { - let prevComment: OctokitComponents["schemas"]["issue-comment"]; + let prevComment: + | OctokitComponents["schemas"]["issue-comment"] + | undefined; + let prevIssueComment: + | OctokitComponents["schemas"]["issue-comment"] + | undefined; + let relatedIssueNumber: number | undefined; + const matchIssueNumber = /(fix|close|resolve)\s*(\d+)/gi; await installation.paginate( "GET /repos/{owner}/{repo}/issues/{issue_number}/comments", @@ -261,12 +270,52 @@ export default eventHandler(async (event) => { prevComment = c; done(); break; + } else { + const body = c.body || ""; + let match; + while ((match = matchIssueNumber.exec(body)) !== null) { + const issueNumber = Number(match[2]); + if (!isNaN(issueNumber)) { + relatedIssueNumber = issueNumber; + break; + } + } + } + if (prevComment) { + if (!syncCommentWithIssue) { + done(); + break; + } else if (relatedIssueNumber) { + done(); + break; + } } } return []; }, ); + if (syncCommentWithIssue && relatedIssueNumber) { + await installation.paginate( + "GET /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner: workflowData.owner, + repo: workflowData.repo, + issue_number: relatedIssueNumber, + }, + ({ data }, done) => { + for (const c of data) { + if (c.performed_via_github_app?.id === Number(appId)) { + prevIssueComment = c; + done(); + break; + } + } + return []; + }, + ); + } + if (comment !== "off") { const { data: { permissions }, @@ -280,49 +329,78 @@ export default eventHandler(async (event) => { try { if (comment === "update" && prevComment!) { + const commentBody = generatePullRequestPublishMessage( + origin, + templatesHtmlMap, + packagesWithoutPrefix, + workflowData, + compact, + onlyTemplates, + checkRunUrl, + packageManager, + commentWithSha ? "sha" : "ref", + bin, + commentWithDev, + ); + await installation.request( "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", { owner: workflowData.owner, repo: workflowData.repo, comment_id: prevComment.id, - body: generatePullRequestPublishMessage( - origin, - templatesHtmlMap, - packagesWithoutPrefix, - workflowData, - compact, - onlyTemplates, - checkRunUrl, - packageManager, - commentWithSha ? "sha" : "ref", - bin, - commentWithDev, - ), + body: commentBody, }, ); + if ( + syncCommentWithIssue && + relatedIssueNumber && + prevIssueComment + ) { + await installation.request( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + { + owner: workflowData.owner, + repo: workflowData.repo, + comment_id: prevIssueComment.id, + body: commentBody, + }, + ); + } } else { + const commentBody = generatePullRequestPublishMessage( + origin, + templatesHtmlMap, + packagesWithoutPrefix, + workflowData, + compact, + onlyTemplates, + checkRunUrl, + packageManager, + comment === "update" ? "ref" : "sha", + bin, + commentWithDev, + ); await installation.request( "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", { owner: workflowData.owner, repo: workflowData.repo, issue_number: Number(workflowData.ref), - body: generatePullRequestPublishMessage( - origin, - templatesHtmlMap, - packagesWithoutPrefix, - workflowData, - compact, - onlyTemplates, - checkRunUrl, - packageManager, - comment === "update" ? "ref" : "sha", - bin, - commentWithDev, - ), + body: commentBody, }, ); + if (syncCommentWithIssue && relatedIssueNumber) { + await installation.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner: workflowData.owner, + repo: workflowData.repo, + issue_number: relatedIssueNumber, + body: commentBody, + }, + ); + } } } catch (error) { console.error("failed to create/update comment", error, permissions); diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 470763e1..ad816cb8 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -100,6 +100,12 @@ const main = defineCommand({ "should install the packages with the 'dev' tag in the comment links", default: false, }, + syncCommentWithIssue: { + type: "boolean", + description: + "sync the comment with the related issue if any (issue number is extracted from the comment body)", + default: false, + }, "only-templates": { type: "boolean", description: `generate only stackblitz templates`, @@ -156,6 +162,7 @@ const main = defineCommand({ const isBinaryApplication = !!args.bin; const isCommentWithSha = !!args.commentWithSha; const isCommentWithDev = !!args.commentWithDev; + const isSyncCommentWithIssue = !!args.syncCommentWithIssue; const comment: Comment = args.comment as Comment; const selectedPackageManager = [ ...new Set( @@ -559,6 +566,7 @@ const main = defineCommand({ "sb-only-templates": `${isOnlyTemplates}`, "sb-comment-with-sha": `${isCommentWithSha}`, "sb-comment-with-dev": `${isCommentWithDev}`, + "sb-sync-comment-with-issue": `${isSyncCommentWithIssue}`, }, body: formData, }); From 94fd458e26d0b66c84a4171e5d0b8486144128c1 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Fri, 12 Dec 2025 22:24:25 +0800 Subject: [PATCH 2/4] feat: match more case --- packages/app/server/routes/publish.post.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/app/server/routes/publish.post.ts b/packages/app/server/routes/publish.post.ts index 59b5c132..0fcffbb0 100644 --- a/packages/app/server/routes/publish.post.ts +++ b/packages/app/server/routes/publish.post.ts @@ -256,6 +256,10 @@ export default eventHandler(async (event) => { | undefined; let relatedIssueNumber: number | undefined; const matchIssueNumber = /(fix|close|resolve)\s*(\d+)/gi; + const fullAddressMatchIssueNumber = new RegExp( + `(fix|close|resolve)\\s*https://github.com/${workflowData.owner}/${workflowData.repo}/issues/(\\d+)`, + "gi", + ); await installation.paginate( "GET /repos/{owner}/{repo}/issues/{issue_number}/comments", @@ -273,6 +277,7 @@ export default eventHandler(async (event) => { } else { const body = c.body || ""; let match; + matchIssueNumber.lastIndex = 0; while ((match = matchIssueNumber.exec(body)) !== null) { const issueNumber = Number(match[2]); if (!isNaN(issueNumber)) { @@ -280,6 +285,18 @@ export default eventHandler(async (event) => { break; } } + if (!relatedIssueNumber) { + fullAddressMatchIssueNumber.lastIndex = 0; + while ( + (match = fullAddressMatchIssueNumber.exec(body)) !== null + ) { + const issueNumber = Number(match[2]); + if (!isNaN(issueNumber)) { + relatedIssueNumber = issueNumber; + break; + } + } + } } if (prevComment) { if (!syncCommentWithIssue) { From 1c54d0a3d5e9c516537519ea95c33ca0c939d853 Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sun, 21 Dec 2025 08:08:24 +0800 Subject: [PATCH 3/4] fix: update --- packages/app/server/routes/publish.post.ts | 109 +++++++++++---------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/packages/app/server/routes/publish.post.ts b/packages/app/server/routes/publish.post.ts index 0fcffbb0..4f7d6d4f 100644 --- a/packages/app/server/routes/publish.post.ts +++ b/packages/app/server/routes/publish.post.ts @@ -251,13 +251,12 @@ export default eventHandler(async (event) => { let prevComment: | OctokitComponents["schemas"]["issue-comment"] | undefined; - let prevIssueComment: - | OctokitComponents["schemas"]["issue-comment"] - | undefined; - let relatedIssueNumber: number | undefined; - const matchIssueNumber = /(fix|close|resolve)\s*(\d+)/gi; + let prevIssueComments: OctokitComponents["schemas"]["issue-comment"][] = + []; + let relatedIssueNumbers: number[] = []; + const matchIssueNumber = /(fix(es)?|closes?|resolves?)\s*(\d+)/gi; const fullAddressMatchIssueNumber = new RegExp( - `(fix|close|resolve)\\s*https://github.com/${workflowData.owner}/${workflowData.repo}/issues/(\\d+)`, + `(fix(es)|closes?|resolves?)\\s*https://github.com/${workflowData.owner}/${workflowData.repo}/issues/(\\d+)`, "gi", ); @@ -281,19 +280,17 @@ export default eventHandler(async (event) => { while ((match = matchIssueNumber.exec(body)) !== null) { const issueNumber = Number(match[2]); if (!isNaN(issueNumber)) { - relatedIssueNumber = issueNumber; - break; + relatedIssueNumbers.push(issueNumber); } } - if (!relatedIssueNumber) { + if (!relatedIssueNumbers.length) { fullAddressMatchIssueNumber.lastIndex = 0; while ( (match = fullAddressMatchIssueNumber.exec(body)) !== null ) { const issueNumber = Number(match[2]); if (!isNaN(issueNumber)) { - relatedIssueNumber = issueNumber; - break; + relatedIssueNumbers.push(issueNumber); } } } @@ -302,7 +299,7 @@ export default eventHandler(async (event) => { if (!syncCommentWithIssue) { done(); break; - } else if (relatedIssueNumber) { + } else if (relatedIssueNumbers.length) { done(); break; } @@ -312,25 +309,28 @@ export default eventHandler(async (event) => { }, ); - if (syncCommentWithIssue && relatedIssueNumber) { - await installation.paginate( - "GET /repos/{owner}/{repo}/issues/{issue_number}/comments", - { - owner: workflowData.owner, - repo: workflowData.repo, - issue_number: relatedIssueNumber, - }, - ({ data }, done) => { - for (const c of data) { - if (c.performed_via_github_app?.id === Number(appId)) { - prevIssueComment = c; - done(); - break; + if (syncCommentWithIssue && relatedIssueNumbers.length) { + prevIssueComments = []; + for (const issueNumber of relatedIssueNumbers) { + await installation.paginate( + "GET /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner: workflowData.owner, + repo: workflowData.repo, + issue_number: issueNumber, + }, + ({ data }, done) => { + for (const c of data) { + if (c.performed_via_github_app?.id === Number(appId)) { + prevIssueComments.push(c); + done(); + break; + } } - } - return []; - }, - ); + return []; + }, + ); + } } if (comment !== "off") { @@ -371,18 +371,23 @@ export default eventHandler(async (event) => { ); if ( syncCommentWithIssue && - relatedIssueNumber && - prevIssueComment + relatedIssueNumbers.length && + prevIssueComments.length ) { - await installation.request( - "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", - { - owner: workflowData.owner, - repo: workflowData.repo, - comment_id: prevIssueComment.id, - body: commentBody, - }, - ); + for (let i = 0; i < relatedIssueNumbers.length; i++) { + const prevIssueComment = prevIssueComments[i]; + if (!prevIssueComment) continue; + + await installation.request( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + { + owner: workflowData.owner, + repo: workflowData.repo, + comment_id: prevIssueComment.id, + body: commentBody, + }, + ); + } } } else { const commentBody = generatePullRequestPublishMessage( @@ -407,16 +412,18 @@ export default eventHandler(async (event) => { body: commentBody, }, ); - if (syncCommentWithIssue && relatedIssueNumber) { - await installation.request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - { - owner: workflowData.owner, - repo: workflowData.repo, - issue_number: relatedIssueNumber, - body: commentBody, - }, - ); + if (syncCommentWithIssue && relatedIssueNumbers.length) { + for (const relatedIssueNumber of relatedIssueNumbers) { + await installation.request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner: workflowData.owner, + repo: workflowData.repo, + issue_number: relatedIssueNumber, + body: commentBody, + }, + ); + } } } } catch (error) { From 897254500a33969ae009832ce0528fdb59ec676e Mon Sep 17 00:00:00 2001 From: btea <2356281422@qq.com> Date: Sun, 21 Dec 2025 08:13:39 +0800 Subject: [PATCH 4/4] fix: update --- README.md | 2 ++ packages/app/server/routes/publish.post.ts | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42853e08..c4ef8520 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ For repositories with many packages, comments might get too long. In that case, pkg.pr.new uses `npm pack --json` under the hood, in case you face issues, you can also use the `--pnpm`, `--yarn`, or `--bun` flag so it starts using `pnpm pack`, `yarn pack`, or `bun pm pack`. This is not necessary in most cases. +If you want to add comments to the associated issue simultaneously, you can set `--syncCommentWithIssue`. + pkg.pr.new is not available in your local environment and it only works in workflows. diff --git a/packages/app/server/routes/publish.post.ts b/packages/app/server/routes/publish.post.ts index 4f7d6d4f..c10ec973 100644 --- a/packages/app/server/routes/publish.post.ts +++ b/packages/app/server/routes/publish.post.ts @@ -251,9 +251,9 @@ export default eventHandler(async (event) => { let prevComment: | OctokitComponents["schemas"]["issue-comment"] | undefined; - let prevIssueComments: OctokitComponents["schemas"]["issue-comment"][] = + const prevIssueComments: OctokitComponents["schemas"]["issue-comment"][] = []; - let relatedIssueNumbers: number[] = []; + const relatedIssueNumbers: number[] = []; const matchIssueNumber = /(fix(es)?|closes?|resolves?)\s*(\d+)/gi; const fullAddressMatchIssueNumber = new RegExp( `(fix(es)|closes?|resolves?)\\s*https://github.com/${workflowData.owner}/${workflowData.repo}/issues/(\\d+)`, @@ -310,7 +310,6 @@ export default eventHandler(async (event) => { ); if (syncCommentWithIssue && relatedIssueNumbers.length) { - prevIssueComments = []; for (const issueNumber of relatedIssueNumbers) { await installation.paginate( "GET /repos/{owner}/{repo}/issues/{issue_number}/comments",