diff --git a/.github/workflows/q.lock.yml b/.github/workflows/q.lock.yml index 06b29d8151e..b4b9c70d2ed 100644 --- a/.github/workflows/q.lock.yml +++ b/.github/workflows/q.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"88a611341d472af28f0e3b4485bc236d1290d4161b87493afaac100a653c0b45","body_hash":"9b6d2e493f1da6f7752d5ee4bfd2a8e41cdb65637bc938b3dbee3d948a7d5734","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70","copilot-sdk":"1.0.6"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b90dd853b22f849cfac3f8d92485e0e3cb595be1b67cf727a1fa578630050541","body_hash":"9b6d2e493f1da6f7752d5ee4bfd2a8e41cdb65637bc938b3dbee3d948a7d5734","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.70","copilot-sdk":"1.0.6"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"924ae3a1cded613372ab5595356fb5720e22ba16","version":"v6.5.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31","digest":"sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.31@sha256:84d861cb6da723ac10b7a00dddf778be681b8cd74b2091f18ce1d67fe4b3e7a1"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31","digest":"sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.31@sha256:80d982fe7925c640d76cbbfbe94081d2d34f7657b7c37494d8d5488f5dae3c63"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.31","digest":"sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.31@sha256:7c63bc4e57d6eac1be996bb793a5a2d74d40b15a616003f4b6805a457046c673"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31","digest":"sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.31@sha256:c05a3f086946fab0833e078f46d35571080f187ca72f038958d45aa5cc150494"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.1","digest":"sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.1@sha256:ad2a979c2cd8b50098e84938ca9c9c1580eb8e91526f101a90adfba7859b2c32"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.5.0","digest":"sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4","pinned_image":"ghcr.io/github/github-mcp-server:v1.5.0@sha256:e25564dccc9110a70a77b9df560cbde11aa392fcb5f08b9abe5c4ebc6d146ea4"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -81,15 +81,6 @@ on: - opened - edited - reopened - pull_request: - types: - - opened - - edited - - reopened - pull_request_review_comment: - types: - - created - - edited # roles: # Roles processed as role check in pre-activation job # - admin # Roles processed as role check in pre-activation job # - maintainer # Roles processed as role check in pre-activation job @@ -113,7 +104,7 @@ env: jobs: activation: needs: pre_activation - if: "needs.pre_activation.outputs.activated == 'true' && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/q ') || startsWith(github.event.issue.body, '/q\n') || github.event.issue.body == '/q') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/q ') || startsWith(github.event.pull_request.body, '/q\n') || github.event.pull_request.body == '/q') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/q ') || startsWith(github.event.discussion.body, '/q\n') || github.event.discussion.body == '/q') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request')) && (!(github.event_name == 'pull_request_review_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))" + if: "needs.pre_activation.outputs.activated == 'true' && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/q ') || startsWith(github.event.issue.body, '/q\n') || github.event.issue.body == '/q') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request != null || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/q ') || startsWith(github.event.discussion.body, '/q\n') || github.event.discussion.body == '/q') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))" runs-on: ubuntu-slim permissions: actions: read @@ -1815,7 +1806,7 @@ jobs: } pre_activation: - if: "(github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/q ') || startsWith(github.event.issue.body, '/q\n') || github.event.issue.body == '/q') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/q ') || startsWith(github.event.pull_request.body, '/q\n') || github.event.pull_request.body == '/q') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/q ') || startsWith(github.event.discussion.body, '/q\n') || github.event.discussion.body == '/q') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request')) && (!(github.event_name == 'pull_request_review_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))" + if: "(github.event_name != 'issue_comment' || contains(fromJSON('[\"OWNER\",\"MEMBER\",\"COLLABORATOR\"]'), github.event.comment.author_association)) && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/q ') || startsWith(github.event.issue.body, '/q\n') || github.event.issue.body == '/q') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q') && github.event.issue.pull_request != null || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/q ') || startsWith(github.event.discussion.body, '/q\n') || github.event.discussion.body == '/q') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/q ') || startsWith(github.event.comment.body, '/q\n') || github.event.comment.body == '/q')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))" runs-on: ubuntu-slim permissions: contents: read diff --git a/.github/workflows/q.md b/.github/workflows/q.md index 234999151f5..43dc7301176 100644 --- a/.github/workflows/q.md +++ b/.github/workflows/q.md @@ -6,6 +6,7 @@ on: roles: [admin, maintainer, write] slash_command: name: q + events: [issues, issue_comment, pull_request_comment, discussion, discussion_comment] reaction: rocket status-comment: true permissions: diff --git a/pkg/workflow/compiler_pre_activation_job.go b/pkg/workflow/compiler_pre_activation_job.go index 51d76b0f683..4aaddfc1fd0 100644 --- a/pkg/workflow/compiler_pre_activation_job.go +++ b/pkg/workflow/compiler_pre_activation_job.go @@ -519,7 +519,7 @@ func (c *Compiler) applyPreActivationIfConditionGuards(data *WorkflowData, needs // 2. The compiled on: section itself contains a GitHub Actions expression (contains ${{): // event detection cannot be performed reliably at compile time. if needsPermissionCheck && hasCommentEventInOn(data.On) && !botsContainExpression(data.Bots) && !strings.Contains(data.On, "${{") { - jobIfCondition = combinePreActivationIfCondition(RenderCondition(buildCommentAuthorAssociationCondition(data.Bots)), jobIfCondition) + jobIfCondition = combinePreActivationIfCondition(RenderCondition(buildCommentAuthorAssociationCondition(data.Bots, activeCommentEventsInOn(data.On))), jobIfCondition) } // Add optional skip-author-associations event guards as a job-level if condition. // This compiles to a static expression so skipped runs exit early without pre-activation @@ -577,6 +577,27 @@ func hasCommentEventInOn(on string) bool { return strings.Contains(on, "issue_comment:") || strings.Contains(on, "pull_request_review_comment:") } +// activeCommentEventsInOn returns the subset of guarded comment event names +// ("issue_comment", "pull_request_review_comment") that are present as trigger +// keys in the rendered on: section. The result is used to emit a precise +// author_association guard that only references events actually in the workflow. +// +// The on: string is compiler-generated YAML whose trigger keys always appear as +// top-level YAML keys followed immediately by a colon (e.g. "issue_comment:\n"). +// Matching ":" therefore reliably identifies a trigger key without +// false-positives from embedded user strings or comments — the same pattern used +// by the pre-existing hasCommentEventInOn helper. +func activeCommentEventsInOn(on string) []string { + var events []string + if strings.Contains(on, "issue_comment:") { + events = append(events, "issue_comment") + } + if strings.Contains(on, "pull_request_review_comment:") { + events = append(events, "pull_request_review_comment") + } + return events +} + // botsContainExpression reports whether any entry in bots is a GitHub Actions expression // (i.e. contains "${{"). When true, the static author_association guard must be disabled so // that check_membership always runs and evaluates the bot list at runtime. diff --git a/pkg/workflow/expression_builder.go b/pkg/workflow/expression_builder.go index cfc78bb861d..6745056fd03 100644 --- a/pkg/workflow/expression_builder.go +++ b/pkg/workflow/expression_builder.go @@ -3,6 +3,7 @@ package workflow import ( "encoding/json" "fmt" + "slices" "sort" "strings" @@ -164,11 +165,22 @@ func buildDispatchSourceEventCondition(includeIssues bool, includePullRequests b // Actors listed in bots (from on.bots) are also exempted so that bot/app-triggered workflows // continue to work even though bots rarely carry an OWNER/MEMBER/COLLABORATOR association. // -// The generated expression (without bots) is: +// activeCommentEvents lists which comment events are actually in the compiled on: section +// (e.g. ["issue_comment", "pull_request_review_comment"]). Only events present in this list +// get a "not equal" guard clause; omitting an event that is not a trigger avoids dead-code +// conditions and keeps the generated expression consistent with the workflow's actual triggers. +// +// The generated expression for activeCommentEvents=["issue_comment","pull_request_review_comment"] +// (without bots) is: // // (github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment') // || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) // +// For activeCommentEvents=["issue_comment"] the expression simplifies to: +// +// github.event_name != 'issue_comment' +// || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) +// // With one or more bots an additional OR clause is appended for each bot: // // || github.actor == 'dependabot[bot]' @@ -176,16 +188,36 @@ func buildDispatchSourceEventCondition(includeIssues bool, includePullRequests b // This satisfies the RGS-004 rule (explicit author_association check for comment-triggered // workflows) while remaining transparent to non-comment events such as push or schedule, // and preserves existing on.bots allow-list behaviour. -func buildCommentAuthorAssociationCondition(bots []string) ConditionNode { - notIssueComment := BuildNotEquals( - BuildPropertyAccess("github.event_name"), - BuildStringLiteral("issue_comment"), - ) - notPRReviewComment := BuildNotEquals( - BuildPropertyAccess("github.event_name"), - BuildStringLiteral("pull_request_review_comment"), - ) - notCommentEvent := BuildAnd(notIssueComment, notPRReviewComment) +func buildCommentAuthorAssociationCondition(bots []string, activeCommentEvents []string) ConditionNode { + // Build "not equal" guards only for comment events that are active in this workflow. + // The canonical guarded events are issue_comment and pull_request_review_comment. + guardedEvents := []string{"issue_comment", "pull_request_review_comment"} + var notCommentTerms []ConditionNode + for _, ev := range guardedEvents { + if slices.Contains(activeCommentEvents, ev) { + notCommentTerms = append(notCommentTerms, BuildNotEquals( + BuildPropertyAccess("github.event_name"), + BuildStringLiteral(ev), + )) + } + } + + var notCommentEvent ConditionNode + switch len(notCommentTerms) { + case 0: + // No guarded comment events are active; the condition is always true. + notCommentEvent = BuildBooleanLiteral(true) + case 1: + notCommentEvent = notCommentTerms[0] + default: + // Combine all terms with AND regardless of how many there are, so that + // adding a third guarded event type in the future doesn't silently drop it. + combined := notCommentTerms[0] + for _, term := range notCommentTerms[1:] { + combined = BuildAnd(combined, term) + } + notCommentEvent = combined + } authorizedAssoc := BuildFunctionCall( "contains",