Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/dependabot-go-checker.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .github/workflows/plan.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion .github/workflows/semantic-function-refactor.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions actions/setup/js/close_discussion.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ async function main(config = {}) {
const requiredTitlePrefix = config.required_title_prefix || "";
const maxCount = config.max || 10;
const githubClient = await createAuthenticatedGitHubClient(config);
const allowBody = config.allow_body !== false; // default true; false only when explicitly set to false
let allowedMentionAliases = [];
if (Array.isArray(config.allowedMentionAliases)) {
allowedMentionAliases = config.allowedMentionAliases;
Expand All @@ -174,7 +175,7 @@ async function main(config = {}) {
// Check if we're in staged mode
const isStaged = isStagedMode(config);

core.info(`Close discussion configuration: max=${maxCount}`);
core.info(`Close discussion configuration: max=${maxCount}, allow_body=${allowBody}`);
if (requiredLabels.length > 0) {
core.info(`Required labels: ${requiredLabels.join(", ")}`);
}
Expand Down Expand Up @@ -278,9 +279,18 @@ async function main(config = {}) {
};
}

// Add comment if body is provided
// Add comment if body is provided and allow-body is not false
let commentUrl;
if (item.body) {
if (!allowBody) {
// allow-body: false — drop any body the agent provided and close without a comment
if (item.body) {
core.warning(
`close_discussion: allow-body is false — dropping non-empty body (length=${item.body.length}) and closing without a comment`
);
} else {
core.info("close_discussion: allow-body is false — closing without a comment");
}
} else if (item.body) {
const sanitizedBody = sanitizeContent(item.body, { allowedAliases: allowedMentionAliases });
const comment = await addDiscussionComment(githubClient, discussion.id, sanitizedBody);
core.info(`Added comment to discussion #${discussionNumber}: ${comment.url}`);
Expand Down
135 changes: 135 additions & 0 deletions actions/setup/js/close_discussion.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -528,5 +528,140 @@ describe("close_discussion", () => {
expect(result.error).toContain("aw_disc1");
});
});

describe("allow-body: false", () => {
it("should close without comment when allow-body is false and body is empty", async () => {
const handler = await main({ max: 10, allow_body: false });
let commentCalled = false;
let closeCalled = false;

mockGithub.graphql = async (/** @type {string} */ query) => {
if (query.includes("addDiscussionComment")) {
commentCalled = true;
return { addDiscussionComment: { comment: { id: "DC_1", url: "url" } } };
}
if (query.includes("closeDiscussion")) {
closeCalled = true;
return { closeDiscussion: { discussion: { id: "D_1", url: "https://github.com/owner/repo/discussions/42" } } };
}
return {
repository: {
discussion: {
id: "D_kwDOTest123",
title: "Test Discussion",
closed: false,
category: { name: "General" },
url: "https://github.com/owner/repo/discussions/42",
labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } },
},
},
};
};

const result = await handler({ body: "" }, {});

expect(result.success).toBe(true);
expect(commentCalled).toBe(false);
expect(closeCalled).toBe(true);
expect(mockCore.infos.some(msg => msg.includes("allow-body is false"))).toBe(true);
});

it("should close without comment and warn when allow-body is false and agent provides non-empty body", async () => {
const handler = await main({ max: 10, allow_body: false });
let commentCalled = false;

mockGithub.graphql = async (/** @type {string} */ query) => {
if (query.includes("addDiscussionComment")) {
commentCalled = true;
return { addDiscussionComment: { comment: { id: "DC_1", url: "url" } } };
}
if (query.includes("closeDiscussion")) {
return { closeDiscussion: { discussion: { id: "D_1", url: "https://github.com/owner/repo/discussions/42" } } };
}
return {
repository: {
discussion: {
id: "D_kwDOTest123",
title: "Test Discussion",
closed: false,
category: { name: "General" },
url: "https://github.com/owner/repo/discussions/42",
labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } },
},
},
};
};

const result = await handler({ body: "This summary should be dropped" }, {});

expect(result.success).toBe(true);
expect(commentCalled).toBe(false);
expect(mockCore.warnings.some(msg => msg.includes("allow-body is false") && msg.includes("dropping"))).toBe(true);
});

it("should still add comment when allow-body is not set (default behavior)", async () => {
const handler = await main({ max: 10 });
let commentCalled = false;

mockGithub.graphql = async (/** @type {string} */ query) => {
if (query.includes("addDiscussionComment")) {
commentCalled = true;
return { addDiscussionComment: { comment: { id: "DC_1", url: "url" } } };
}
if (query.includes("closeDiscussion")) {
return { closeDiscussion: { discussion: { id: "D_1", url: "https://github.com/owner/repo/discussions/42" } } };
}
return {
repository: {
discussion: {
id: "D_kwDOTest123",
title: "Test Discussion",
closed: false,
category: { name: "General" },
url: "https://github.com/owner/repo/discussions/42",
labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } },
},
},
};
};

const result = await handler({ body: "Closing summary" }, {});

expect(result.success).toBe(true);
expect(commentCalled).toBe(true);
});

it("should still add comment when allow-body is explicitly true", async () => {
const handler = await main({ max: 10, allow_body: true });
let commentCalled = false;

mockGithub.graphql = async (/** @type {string} */ query) => {
if (query.includes("addDiscussionComment")) {
commentCalled = true;
return { addDiscussionComment: { comment: { id: "DC_1", url: "url" } } };
}
if (query.includes("closeDiscussion")) {
return { closeDiscussion: { discussion: { id: "D_1", url: "https://github.com/owner/repo/discussions/42" } } };
}
return {
repository: {
discussion: {
id: "D_kwDOTest123",
title: "Test Discussion",
closed: false,
category: { name: "General" },
url: "https://github.com/owner/repo/discussions/42",
labels: { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } },
},
},
};
};

const result = await handler({ body: "Closing summary" }, {});

expect(result.success).toBe(true);
expect(commentCalled).toBe(true);
});
});
});
});
75 changes: 46 additions & 29 deletions actions/setup/js/close_entity_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ function createCloseEntityHandler(config, entityConfig, callbacks, githubClient)
const comment = config.comment || "";
const isStaged = isStagedMode(config);
const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config);
const allowBody = config.allow_body !== false; // default true; false only when explicitly set to false

let processedCount = 0;

Expand All @@ -293,12 +294,22 @@ function createCloseEntityHandler(config, entityConfig, callbacks, githubClient)
core.info(`Processing ${entityConfig.itemType} message: ${JSON.stringify(logFields)}`);

// 2. Comment body resolution
/** @type {string} */
/** @type {string|undefined} */
let commentToPost;
/** @type {string} */
let commentSource = "unknown";

if (typeof item.body === "string" && item.body.trim() !== "") {
if (!allowBody) {
// allow-body: false — drop any body the agent provided and skip the comment
if (typeof item.body === "string" && item.body.trim() !== "") {
core.warning(
`${entityConfig.itemType}: allow-body is false — dropping non-empty body (length=${item.body.length}) and closing without a comment`
);
} else {
core.info(`${entityConfig.itemType}: allow-body is false — closing without a comment`);
}
commentToPost = undefined;
} else if (typeof item.body === "string" && item.body.trim() !== "") {
commentToPost = item.body;
commentSource = "item.body";
} else if (typeof comment === "string" && comment.trim() !== "") {
Expand All @@ -309,10 +320,12 @@ function createCloseEntityHandler(config, entityConfig, callbacks, githubClient)
return { success: false, error: "No comment body provided" };
}

core.info(`Comment body determined: length=${commentToPost.length}, source=${commentSource}`);
if (commentToPost !== undefined) {
core.info(`Comment body determined: length=${commentToPost.length}, source=${commentSource}`);

// 3. Content sanitization
commentToPost = sanitizeContent(commentToPost);
// 3. Content sanitization
commentToPost = sanitizeContent(commentToPost);
}

// 4. Target repository / entity number resolution
const targetResult = callbacks.resolveTarget(item, config, resolvedTemporaryIds);
Expand Down Expand Up @@ -379,34 +392,38 @@ function createCloseEntityHandler(config, entityConfig, callbacks, githubClient)
};
}

// 9. Comment posting
const commentBody = callbacks.buildCommentBody(commentToPost, item);
core.info(`Adding comment to ${entityConfig.displayName} #${entityNumber}: length=${commentBody.length}`);

// 9. Comment posting (skipped when allow-body: false or no body available)
/** @type {{id: number, html_url: string}|null} */
let commentResult = null;
let commentPosted = false;
try {
commentResult = await callbacks.addComment(githubClient, owner, repoName, entityNumber, commentBody);
commentPosted = true;
core.info(`✓ Comment posted to ${entityConfig.displayName} #${entityNumber}: ${commentResult.html_url}`);
core.info(`Comment details: id=${commentResult.id}, body_length=${commentBody.length}`);
} catch (commentError) {
const errorMsg = getErrorMessage(commentError);
if (callbacks.continueOnCommentError) {
core.error(`Failed to add comment to ${entityConfig.displayName} #${entityNumber}: ${errorMsg}`);
core.error(
`Error details: ${JSON.stringify({
entityNumber,
hasBody: !!item.body,
bodyLength: item.body ? item.body.length : 0,
errorMessage: errorMsg,
})}`
);
// commentPosted stays false; close operation continues
} else {
throw new Error(`${ERR_API}: Failed to add comment to ${entityConfig.displayName} #${entityNumber}: ${errorMsg}`, { cause: commentError });
if (commentToPost !== undefined) {
const commentBody = callbacks.buildCommentBody(commentToPost, item);
core.info(`Adding comment to ${entityConfig.displayName} #${entityNumber}: length=${commentBody.length}`);

try {
commentResult = await callbacks.addComment(githubClient, owner, repoName, entityNumber, commentBody);
commentPosted = true;
core.info(`✓ Comment posted to ${entityConfig.displayName} #${entityNumber}: ${commentResult.html_url}`);
core.info(`Comment details: id=${commentResult.id}, body_length=${commentBody.length}`);
} catch (commentError) {
const errorMsg = getErrorMessage(commentError);
if (callbacks.continueOnCommentError) {
core.error(`Failed to add comment to ${entityConfig.displayName} #${entityNumber}: ${errorMsg}`);
core.error(
`Error details: ${JSON.stringify({
entityNumber,
hasBody: !!item.body,
bodyLength: item.body ? item.body.length : 0,
errorMessage: errorMsg,
})}`
);
// commentPosted stays false; close operation continues
} else {
throw new Error(`${ERR_API}: Failed to add comment to ${entityConfig.displayName} #${entityNumber}: ${errorMsg}`, { cause: commentError });
}
}
} else {
core.info(`Skipping comment for ${entityConfig.displayName} #${entityNumber}: no comment body`);
}

// 10. Entity close (skipped when already closed)
Expand Down
73 changes: 73 additions & 0 deletions actions/setup/js/close_issue.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -791,4 +791,77 @@ describe("close_issue", () => {
expect(result.error).toContain("aw_pending");
});
});

describe("allow-body: false", () => {
it("should close without comment when allow-body is false and body is empty", async () => {
const handler = await main({ max: 10, allow_body: false });
const commentCalls = [];

mockGithub.rest.issues.createComment = async params => {
commentCalls.push(params);
return { data: { id: 1, html_url: "url" } };
};

const result = await handler({ issue_number: 100, body: "" }, {});

expect(result.success).toBe(true);
expect(commentCalls.length).toBe(0);
expect(mockCore.infos.some(msg => msg.includes("allow-body is false"))).toBe(true);
});

it("should close without comment and warn when allow-body is false and agent provides non-empty body", async () => {
const handler = await main({ max: 10, allow_body: false });
const commentCalls = [];

mockGithub.rest.issues.createComment = async params => {
commentCalls.push(params);
return { data: { id: 1, html_url: "url" } };
};

const result = await handler({ issue_number: 100, body: "This summary should be dropped" }, {});

expect(result.success).toBe(true);
expect(commentCalls.length).toBe(0);
expect(mockCore.warnings.some(msg => msg.includes("allow-body is false") && msg.includes("dropping"))).toBe(true);
});

it("should close without comment when allow-body is false and no body is provided", async () => {
const handler = await main({ max: 10, allow_body: false });
const commentCalls = [];

mockGithub.rest.issues.createComment = async params => {
commentCalls.push(params);
return { data: { id: 1, html_url: "url" } };
};

const result = await handler({ issue_number: 100 }, {});

expect(result.success).toBe(true);
expect(commentCalls.length).toBe(0);
});

it("should still require body when allow-body is not set (default behavior)", async () => {
const handler = await main({ max: 10 });

const result = await handler({ issue_number: 100 }, {});

expect(result.success).toBe(false);
expect(result.error).toContain("No comment body provided");
});

it("should still add comment when allow-body is explicitly true", async () => {
const handler = await main({ max: 10, allow_body: true });
const commentCalls = [];

mockGithub.rest.issues.createComment = async params => {
commentCalls.push(params);
return { data: { id: 1, html_url: "url" } };
};

const result = await handler({ issue_number: 100, body: "Closing summary" }, {});

expect(result.success).toBe(true);
expect(commentCalls.length).toBe(1);
});
});
});
Loading