fix(skills): use discussion target for notifications

Use the discussion node ID for discussion_comment notifications and update the skill docs to reflect the notify target semantics.
This commit is contained in:
Mason Huang
2026-04-14 09:11:57 +08:00
parent 0ede74627f
commit bfee3f6f09
2 changed files with 56 additions and 26 deletions

View File

@@ -64,16 +64,16 @@ The `fetch-content` output includes:
### Location type routing
| type | Flow |
| ----------------------------- | ------------------------ |
| `issue_comment` | Comment: delete+recreate |
| `pull_request_comment` | Comment: delete+recreate |
| `pull_request_review_comment` | Comment: delete+recreate |
| type | Flow |
| ----------------------------- | --------------------------------------------- |
| `issue_comment` | Comment: delete+recreate |
| `pull_request_comment` | Comment: delete+recreate |
| `pull_request_review_comment` | Comment: delete+recreate |
| `discussion_comment` | Discussion comment: delete+recreate (GraphQL) |
| `issue_body` | Body: redact in place |
| `pull_request_body` | Body: redact in place |
| `commit` | Notify only |
| _other_ | Skip and report |
| `issue_body` | Body: redact in place |
| `pull_request_body` | Body: redact in place |
| `commit` | Notify only |
| _other_ | Skip and report |
## Step 2: Decide (Agent)
@@ -102,6 +102,7 @@ node secret-scanning.mjs redact-body <issue|pr> <NUMBER> <redacted-body-file>
### Comments — Delete and Recreate
For issue/PR comments:
```bash
# Delete original (all edit history gone)
node secret-scanning.mjs delete-comment <COMMENT_ID>
@@ -111,6 +112,7 @@ node secret-scanning.mjs recreate-comment <ISSUE_NUMBER> <body-file>
```
For discussion comments (uses GraphQL):
```bash
# Delete original
node secret-scanning.mjs delete-discussion-comment <COMMENT_NODE_ID>
@@ -152,9 +154,12 @@ Cannot clean. Notify author to delete branch or force-push (for unmerged PRs).
## Step 5: Notify
```bash
node secret-scanning.mjs notify <ISSUE_NUMBER> <AUTHOR> <LOCATION_TYPE> <SECRET_TYPES>
node secret-scanning.mjs notify <TARGET> <AUTHOR> <LOCATION_TYPE> <SECRET_TYPES>
```
- For non-discussion types, `<TARGET>` is the issue/PR number.
- For `discussion_comment`, `<TARGET>` is the `discussion_node_id` returned by `fetch-content`.
Secret types are comma-separated: `"Discord Bot Token,Feishu App Secret"`
The script picks the right template:

View File

@@ -114,7 +114,8 @@ function cmdFetchContent(locationJson) {
while (hasNextPage && !comment) {
const afterClause = cursor ? `, after: "${cursor}"` : "";
const gql = ghGraphQL(`{
const gql = ghGraphQL(
`{
repository(owner: "${owner}", name: "${name}") {
discussion(number: ${discussionNumber}) {
id
@@ -131,10 +132,15 @@ function cmdFetchContent(locationJson) {
}
}
}
}`, { allowFailure: true });
}`,
{ allowFailure: true },
);
const discussion = gql?.data?.repository?.discussion;
if (!discussion) fail(`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`);
if (!discussion)
fail(
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
);
discussionId = discussion.id;
comment = discussion.comments.nodes.find(
@@ -144,7 +150,10 @@ function cmdFetchContent(locationJson) {
cursor = discussion.comments.pageInfo.endCursor;
}
if (!comment) fail(`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`);
if (!comment)
fail(
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
);
const bodyFile = tmpFile("body.md");
fs.writeFileSync(bodyFile, comment.body || "");
@@ -356,7 +365,9 @@ function cmdDeleteComment(commentId) {
*/
function cmdDeleteDiscussionComment(nodeId) {
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
const result = ghGraphQL(`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`);
const result = ghGraphQL(
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
);
if (result?.errors) {
fail(`Failed to delete discussion comment: ${JSON.stringify(result.errors)}`);
}
@@ -368,13 +379,20 @@ function cmdDeleteDiscussionComment(nodeId) {
* Create a new discussion comment via GraphQL.
*/
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile) {
if (!discussionNodeId || !bodyFile) fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file>");
if (!discussionNodeId || !bodyFile)
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file>");
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
const body = fs.readFileSync(bodyFile, "utf8");
// Escape for GraphQL string literal
const escapedBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
const result = ghGraphQL(`mutation { addDiscussionComment(input: { discussionId: "${discussionNodeId}", body: "${escapedBody}" }) { comment { id url } } }`);
const escapedBody = body
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\r/g, "\\r")
.replace(/\n/g, "\\n");
const result = ghGraphQL(
`mutation { addDiscussionComment(input: { discussionId: "${discussionNodeId}", body: "${escapedBody}" }) { comment { id url } } }`,
);
if (result?.errors) {
fail(`Failed to create discussion comment: ${JSON.stringify(result.errors)}`);
}
@@ -415,12 +433,13 @@ function cmdRecreateComment(issueNumber, bodyFile) {
}
/**
* notify <issue-or-pr-number> <author> <location-type> <secret-types>
* notify <target> <author> <location-type> <secret-types>
* Post a notification comment with the correct template for the location type.
* target = issue/PR number for non-discussion types, discussion node ID for discussion_comment.
*/
function cmdNotify(issueNumber, author, locationType, secretTypes) {
if (!issueNumber || !author || !locationType || !secretTypes) {
fail("Usage: notify <issue-or-pr-number> <author> <location-type> <secret-types-comma-sep>");
function cmdNotify(target, author, locationType, secretTypes) {
if (!target || !author || !locationType || !secretTypes) {
fail("Usage: notify <target> <author> <location-type> <secret-types-comma-sep>");
}
const types = secretTypes.split(",").map((s) => s.trim());
@@ -468,8 +487,14 @@ function cmdNotify(issueNumber, author, locationType, secretTypes) {
// Discussion comments must be notified via GraphQL
if (locationType === "discussion_comment") {
const escapedBody = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
const result = ghGraphQL(`mutation { addDiscussionComment(input: { discussionId: "${issueNumber}", body: "${escapedBody}" }) { comment { id url } } }`);
const escapedBody = body
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\r/g, "\\r")
.replace(/\n/g, "\\n");
const result = ghGraphQL(
`mutation { addDiscussionComment(input: { discussionId: "${target}", body: "${escapedBody}" }) { comment { id url } } }`,
);
if (result?.errors) {
fail(`Failed to post discussion notification: ${JSON.stringify(result.errors)}`);
}
@@ -490,7 +515,7 @@ function cmdNotify(issueNumber, author, locationType, secretTypes) {
const result = gh([
"api",
`repos/${REPO}/issues/${issueNumber}/comments`,
`repos/${REPO}/issues/${target}/comments`,
"-X",
"POST",
"-F",
@@ -660,7 +685,7 @@ if (!command || !commands[command]) {
" delete-discussion-comment <node-id> Delete a discussion comment (GraphQL)",
" recreate-comment <issue-n> <file> Create replacement comment",
" recreate-discussion-comment <disc-node-id> <file> Create discussion comment (GraphQL)",
" notify <n> <author> <type> <types> Post notification",
" notify <target> <author> <type> <types> Post notification",
" resolve <n> [resolution] [comment] Close alert",
" list-open List open alerts",
" summary <json-file> Print formatted summary",