-
Notifications
You must be signed in to change notification settings - Fork 418
add-comment: support hide-older-comments.match for exact multi-workflow comment minimization
#37977
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
bb81f15
c3fd001
93c08cb
c92f3a8
e4552c0
0f9c050
1125206
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,29 @@ function deduplicateCaseInsensitive(aliases) { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * @param {unknown} value | ||
| * @returns {value is { enabled?: boolean | string, match?: unknown[] }} | ||
| */ | ||
| function isHideOlderCommentsObject(value) { | ||
| return Boolean(value && typeof value === "object" && !Array.isArray(value)); | ||
| } | ||
|
|
||
| /** | ||
| * @param {unknown[]} ids | ||
| * @returns {string[]} | ||
| */ | ||
| function normalizeWorkflowIdList(ids) { | ||
| return [ | ||
| ...new Set( | ||
| ids | ||
| .filter(id => typeof id === "string") | ||
| .map(id => id.trim()) | ||
| .filter(Boolean) | ||
| ), | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve effective event name/payload for native and forwarded contexts. | ||
| * Supports: | ||
|
|
@@ -149,10 +172,10 @@ async function minimizeComment(github, nodeId, reason = "outdated") { | |
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @param {number} issueNumber - Issue/PR number | ||
| * @param {string} workflowId - Workflow ID to search for | ||
| * @param {string[]} workflowIds - Workflow IDs to search for | ||
| * @returns {Promise<Array<{id: number, node_id: string, body: string}>>} | ||
| */ | ||
| async function findCommentsWithTrackerId(github, owner, repo, issueNumber, workflowId) { | ||
| async function findCommentsWithTrackerId(github, owner, repo, issueNumber, workflowIds) { | ||
|
Comment on lines
174
to
+178
|
||
| const comments = []; | ||
| let page = 1; | ||
| const perPage = 100; | ||
|
|
@@ -171,7 +194,7 @@ async function findCommentsWithTrackerId(github, owner, repo, issueNumber, workf | |
| break; | ||
| } | ||
|
|
||
| const filteredComments = data.filter(comment => matchesWorkflowId(comment.body, workflowId)).map(({ id, node_id, body }) => ({ id, node_id, body })); | ||
| const filteredComments = data.filter(comment => workflowIds.some(id => matchesWorkflowId(comment.body, id))).map(({ id, node_id, body }) => ({ id, node_id, body })); | ||
|
|
||
| comments.push(...filteredComments); | ||
|
|
||
|
|
@@ -191,10 +214,10 @@ async function findCommentsWithTrackerId(github, owner, repo, issueNumber, workf | |
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @param {number} discussionNumber - Discussion number | ||
| * @param {string} workflowId - Workflow ID to search for | ||
| * @param {string[]} workflowIds - Workflow IDs to search for | ||
| * @returns {Promise<Array<{id: string, body: string}>>} | ||
| */ | ||
| async function findDiscussionCommentsWithTrackerId(github, owner, repo, discussionNumber, workflowId) { | ||
| async function findDiscussionCommentsWithTrackerId(github, owner, repo, discussionNumber, workflowIds) { | ||
|
Comment on lines
216
to
+220
|
||
| const query = /* GraphQL */ ` | ||
| query ($owner: String!, $repo: String!, $num: Int!, $cursor: String) { | ||
| repository(owner: $owner, name: $repo) { | ||
|
|
@@ -224,7 +247,7 @@ async function findDiscussionCommentsWithTrackerId(github, owner, repo, discussi | |
| break; | ||
| } | ||
|
|
||
| const filteredComments = result.repository.discussion.comments.nodes.filter(comment => matchesWorkflowId(comment.body, workflowId)).map(({ id, body }) => ({ id, body })); | ||
| const filteredComments = result.repository.discussion.comments.nodes.filter(comment => workflowIds.some(id => matchesWorkflowId(comment.body, id))).map(({ id, body }) => ({ id, body })); | ||
|
|
||
| comments.push(...filteredComments); | ||
|
|
||
|
|
@@ -244,15 +267,15 @@ async function findDiscussionCommentsWithTrackerId(github, owner, repo, discussi | |
| * @param {string} owner - Repository owner | ||
| * @param {string} repo - Repository name | ||
| * @param {number} itemNumber - Issue/PR/Discussion number | ||
| * @param {string} workflowId - Workflow ID to match | ||
| * @param {string[]} workflowIds - Workflow IDs to match | ||
| * @param {boolean} isDiscussion - Whether this is a discussion | ||
| * @param {string} reason - Reason for hiding (default: outdated) | ||
| * @param {string[] | null} allowedReasons - List of allowed reasons (default: null for all) | ||
| * @returns {Promise<number>} Number of comments hidden | ||
| */ | ||
| async function hideOlderComments(github, owner, repo, itemNumber, workflowId, isDiscussion, reason = "outdated", allowedReasons = null) { | ||
| if (!workflowId) { | ||
| core.info("No workflow ID available, skipping hide-older-comments"); | ||
| async function hideOlderComments(github, owner, repo, itemNumber, workflowIds, isDiscussion, reason = "outdated", allowedReasons = null) { | ||
| if (!Array.isArray(workflowIds) || workflowIds.length === 0) { | ||
| core.info("No workflow IDs provided, skipping hide-older-comments"); | ||
| return 0; | ||
| } | ||
|
|
||
|
|
@@ -268,13 +291,13 @@ async function hideOlderComments(github, owner, repo, itemNumber, workflowId, is | |
| } | ||
| } | ||
|
|
||
| core.info(`Searching for previous comments with workflow ID: ${workflowId}`); | ||
| core.info(`Searching for previous comments with workflow IDs: ${workflowIds.join(", ")}`); | ||
|
|
||
| let comments; | ||
| if (isDiscussion) { | ||
| comments = await findDiscussionCommentsWithTrackerId(github, owner, repo, itemNumber, workflowId); | ||
| comments = await findDiscussionCommentsWithTrackerId(github, owner, repo, itemNumber, workflowIds); | ||
| } else { | ||
| comments = await findCommentsWithTrackerId(github, owner, repo, itemNumber, workflowId); | ||
| comments = await findCommentsWithTrackerId(github, owner, repo, itemNumber, workflowIds); | ||
| } | ||
|
|
||
| if (comments.length === 0) { | ||
|
|
@@ -375,7 +398,13 @@ async function commentOnDiscussion(github, owner, repo, discussionNumber, messag | |
| */ | ||
| async function main(config = {}) { | ||
| // Extract configuration | ||
| const hideOlderCommentsEnabled = parseBoolTemplatable(config.hide_older_comments, false); | ||
| const hideOlderCommentsConfig = isHideOlderCommentsObject(config.hide_older_comments) ? config.hide_older_comments : null; | ||
| const hideOlderCommentsEnabled = parseBoolTemplatable(hideOlderCommentsConfig ? (hideOlderCommentsConfig.enabled ?? true) : config.hide_older_comments, false); | ||
| const hideOlderCommentsMatch = Array.isArray(hideOlderCommentsConfig?.match) | ||
| ? normalizeWorkflowIdList(hideOlderCommentsConfig.match) | ||
| : Array.isArray(config.hide_older_comments_match) | ||
| ? normalizeWorkflowIdList(config.hide_older_comments_match) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The 💡 Suggested test (compiled config form)// mirrors what the Go registry actually sends to the JS handler
const handler = await eval(`(async () => {
${addCommentScript};
return await main({
hide_older_comments: "true",
hide_older_comments_match: ["other_workflow"]
});
})()`);
const result = await handler({ type: "add_comment", body: "compiled path test" }, {});
expect(hiddenNodeIds).toContain("IC_kwDOTest11"); // other_workflowThis mirrors the actual runtime values set by |
||
| : []; | ||
| const commentTarget = config.target || "triggering"; | ||
| const maxCount = config.max || 20; | ||
| const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); | ||
|
|
@@ -406,6 +435,9 @@ async function main(config = {}) { | |
| if (requiredTitlePrefix) core.info(`Required title prefix: ${requiredTitlePrefix}`); | ||
| if (hideOlderCommentsEnabled) { | ||
| core.info("Hide-older-comments is enabled"); | ||
| if (hideOlderCommentsMatch.length > 0) { | ||
| core.info(`Hide-older-comments additional workflow matches: ${hideOlderCommentsMatch.join(", ")}`); | ||
| } | ||
| } | ||
| if (appendOnlyComments) { | ||
| core.info("Append-only-comments is enabled - will not hide older comments"); | ||
|
|
@@ -776,8 +808,9 @@ async function main(config = {}) { | |
| core.info("Skipping hide-older-comments because an existing comment is being updated"); | ||
| } else if (appendOnlyComments) { | ||
| core.info("Skipping hide-older-comments because append-only-comments is enabled"); | ||
| } else if (workflowId) { | ||
| await hideOlderComments(githubClient, repoParts.owner, repoParts.repo, itemNumber, workflowId, isDiscussion); | ||
| } else { | ||
| const hideWorkflowIds = normalizeWorkflowIdList([workflowId, ...hideOlderCommentsMatch]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] When 💡 Suggested testit("should hide match-list comments when GH_AW_WORKFLOW_ID is not set", async () => {
delete process.env.GH_AW_WORKFLOW_ID;
// ... setup mocks ...
const handler = await eval(`... main({ hide_older_comments: { match: ["target-wf"] } }) ...`);
const result = await handler({ type: "add_comment", body: "test" }, {});
expect(hiddenNodeIds).toContain(/* target-wf comment node */);
expect(hiddenNodeIds).not.toContain(/* unrelated comment node */);
}); |
||
| await hideOlderComments(githubClient, repoParts.owner, repoParts.repo, itemNumber, hideWorkflowIds, isDiscussion); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| package workflow | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/github/gh-aw/pkg/logger" | ||
| ) | ||
|
|
||
|
|
@@ -17,11 +19,12 @@ type AddCommentsConfig struct { | |
| TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository comments | ||
| AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that comments can be added to (additionally to the target-repo) | ||
| HideOlderComments *string `yaml:"hide-older-comments,omitempty"` // When true, minimizes/hides all previous comments from the same workflow before creating the new comment | ||
| AllowedReasons []string `yaml:"allowed-reasons,omitempty"` // List of allowed reasons for hiding older comments (default: all reasons allowed) | ||
| Issues *bool `yaml:"issues,omitempty"` // When false, excludes issues:write permission and issues from event condition. Default (nil or true) includes issues:write. | ||
| PullRequests *bool `yaml:"pull-requests,omitempty"` // When false, excludes pull-requests:write permission and PRs from event condition. Default (nil or true) includes pull-requests:write. | ||
| Discussions *bool `yaml:"discussions,omitempty"` // When false, excludes discussions:write permission. Default (nil or true) includes discussions:write. | ||
| Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. | ||
| HideOlderCommentsMatch []string `yaml:"hide-older-comments-match,omitempty"` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/grill-with-docs] The 💡 Suggested comment// HideOlderCommentsMatch is an internal field populated by preprocessHideOlderCommentsConfig
// from the hide-older-comments.match array. Not a user-facing YAML key; the schema exposes
// it only via the hide-older-comments object form.
HideOlderCommentsMatch []string `yaml:"hide-older-comments-match,omitempty"` |
||
| AllowedReasons []string `yaml:"allowed-reasons,omitempty"` // List of allowed reasons for hiding older comments (default: all reasons allowed) | ||
|
|
||
| Issues *bool `yaml:"issues,omitempty"` // When false, excludes issues:write permission and issues from event condition. Default (nil or true) includes issues:write. | ||
| PullRequests *bool `yaml:"pull-requests,omitempty"` // When false, excludes pull-requests:write permission and PRs from event condition. Default (nil or true) includes pull-requests:write. | ||
| Discussions *bool `yaml:"discussions,omitempty"` // When false, excludes discussions:write permission. Default (nil or true) includes discussions:write. | ||
| Footer *string `yaml:"footer,omitempty"` // Controls whether AI-generated footer is added. When false, visible footer is omitted but XML markers are kept. | ||
| } | ||
|
|
||
| // parseCommentsConfig handles add-comment configuration | ||
|
|
@@ -34,6 +37,11 @@ func (c *Compiler) parseCommentsConfig(outputMap map[string]any) *AddCommentsCon | |
| // Get config data for pre-processing before YAML unmarshaling | ||
| configData, _ := outputMap["add-comment"].(map[string]any) | ||
|
|
||
| if err := preprocessHideOlderCommentsConfig(configData, addCommentLog); err != nil { | ||
| addCommentLog.Printf("Invalid hide-older-comments configuration: %v", err) | ||
| return nil | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] An invalid 💡 SuggestionConsider following the existing |
||
| } | ||
|
|
||
| // Pre-process templatable bool fields | ||
| if err := preprocessBoolFieldAsString(configData, "hide-older-comments", addCommentLog); err != nil { | ||
| addCommentLog.Printf("Invalid hide-older-comments value: %v", err) | ||
|
|
@@ -73,6 +81,44 @@ func (c *Compiler) parseCommentsConfig(outputMap map[string]any) *AddCommentsCon | |
| return config | ||
| } | ||
|
|
||
| func preprocessHideOlderCommentsConfig(configData map[string]any, debugLog *logger.Logger) error { | ||
| if configData == nil { | ||
| return nil | ||
| } | ||
|
|
||
| raw, exists := configData["hide-older-comments"] | ||
| if !exists || raw == nil { | ||
| return nil | ||
| } | ||
|
|
||
| objectConfig, ok := raw.(map[string]any) | ||
| if !ok { | ||
| return nil | ||
| } | ||
|
|
||
| if enabledRaw, hasEnabled := objectConfig["enabled"]; hasEnabled { | ||
| switch enabled := enabledRaw.(type) { | ||
| case bool: | ||
| configData["hide-older-comments"] = enabled | ||
| case string: | ||
| if !isExpression(enabled) { | ||
| return fmt.Errorf("field %q must be a boolean or a GitHub Actions expression", "hide-older-comments.enabled") | ||
| } | ||
| configData["hide-older-comments"] = enabled | ||
| default: | ||
| return fmt.Errorf("field %q must be a boolean or a GitHub Actions expression", "hide-older-comments.enabled") | ||
| } | ||
| } else { | ||
| configData["hide-older-comments"] = true | ||
| } | ||
|
|
||
| if matchRaw, hasMatch := objectConfig["match"]; hasMatch { | ||
| configData["hide-older-comments-match"] = parseStringSliceAny(matchRaw, debugLog) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // buildAddCommentPermissions computes the permissions for the add_comment job based on config. | ||
| // Issues: nil or true → issues:write (default: true) | ||
| // PullRequests: nil or true → pull-requests:write (default: true) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd]
normalizeWorkflowIdListtrims whitespace and deduplicates entries, but neither behaviour is exercised by any test. A user authoringmatch: [" wf-a ", "wf-a"]should get["wf-a"]— both the dedup and the trim are load-bearing for correctness.💡 Suggested unit-level assertions
Since this is a pure function, it can be tested directly (or via a small dedicated test block without needing a full mock setup):