fix(workflows): branch protection status checks fail when workflow uses on: paths filter (#38237) (#38302)

This commit is contained in:
Giteabot
2026-07-01 13:10:11 -07:00
committed by GitHub
parent b6e409badd
commit a016d678db
7 changed files with 339 additions and 97 deletions
+15 -10
View File
@@ -55,29 +55,34 @@ func ParseScopedWorkflows(sourceCommit *git.Commit) ([]*ParsedScopedWorkflow, er
return parsed, nil
}
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event, returning those whose `on:` matches.
// MatchScopedWorkflows evaluates already-parsed scoped workflows against one consuming event.
// It returns the workflows whose `on:` matches, and those that matched the event but were excluded by a branch/paths filter (filtered).
func MatchScopedWorkflows(
parsed []*ParsedScopedWorkflow,
consumerGitRepo *git.Repository,
consumerCommit *git.Commit,
triggedEvent webhook_module.HookEventType,
payload api.Payloader,
) []*DetectedWorkflow {
workflows := make([]*DetectedWorkflow, 0, len(parsed))
) (matched, filtered []*DetectedWorkflow) {
for _, p := range parsed {
for _, evt := range p.Events {
if evt.IsSchedule() {
// schedule is a non-target for scoped workflows
continue
}
if detectMatched(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
workflows = append(workflows, &DetectedWorkflow{
EntryName: p.EntryName,
TriggerEvent: evt,
Content: p.Content,
})
dwf := &DetectedWorkflow{
EntryName: p.EntryName,
TriggerEvent: evt,
Content: p.Content,
}
switch detectWorkflowMatch(consumerGitRepo, consumerCommit, triggedEvent, payload, evt) {
case detectMatched:
matched = append(matched, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
}
}
return workflows
return matched, filtered
}
+99 -57
View File
@@ -30,6 +30,14 @@ type DetectedWorkflow struct {
Content []byte
}
type detectResult int
const (
detectMatched detectResult = iota // event matched; run normally
detectNotApplicable // event/type doesn't apply; create nothing
detectFilteredOut // matched but excluded by a branch/paths filter; emits a skipped commit status
)
func init() {
model.OnDecodeNodeError = func(node yaml.Node, out any, err error) {
// Log the error instead of panic or fatal.
@@ -172,18 +180,16 @@ func DetectWorkflows(
triggedEvent webhook_module.HookEventType,
payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
) (workflows, schedules, filtered []*DetectedWorkflow, err error) {
_, entries, err := ListWorkflows(commit)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
workflows := make([]*DetectedWorkflow, 0, len(entries))
schedules := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
// one workflow may have multiple events
@@ -203,18 +209,24 @@ func DetectWorkflows(
}
schedules = append(schedules, dwf)
}
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
} else {
dwf := &DetectedWorkflow{
EntryName: entry.Name(),
TriggerEvent: evt,
Content: content,
}
workflows = append(workflows, dwf)
switch detectWorkflowMatch(gitRepo, commit, triggedEvent, payload, evt) {
case detectMatched:
workflows = append(workflows, dwf)
case detectFilteredOut:
filtered = append(filtered, dwf)
case detectNotApplicable:
}
}
}
}
return workflows, schedules, nil
return workflows, schedules, filtered, nil
}
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
@@ -252,9 +264,9 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D
return wfs, nil
}
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
func detectWorkflowMatch(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) detectResult {
if !canGithubEventMatch(evt.Name, triggedEvent) {
return false
return detectNotApplicable
}
switch triggedEvent {
@@ -268,7 +280,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
}
// no special filter parameters for these events, just return true if name matched
return true
return detectMatched
case // push
webhook_module.HookEventPush:
@@ -279,14 +291,20 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
webhook_module.HookEventIssueAssign,
webhook_module.HookEventIssueLabel,
webhook_module.HookEventIssueMilestone:
return matchIssuesEvent(payload.(*api.IssuePayload), evt)
if matchIssuesEvent(payload.(*api.IssuePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // issue_comment
webhook_module.HookEventIssueComment,
// `pull_request_comment` is same as `issue_comment`
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_comment-use-issue_comment
webhook_module.HookEventPullRequestComment:
return matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt)
if matchIssueCommentEvent(payload.(*api.IssueCommentPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request
webhook_module.HookEventPullRequest,
@@ -300,34 +318,49 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
case // pull_request_review
webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected:
return matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt)
if matchPullRequestReviewEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // pull_request_review_comment
webhook_module.HookEventPullRequestReviewComment:
return matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt)
if matchPullRequestReviewCommentEvent(payload.(*api.PullRequestPayload), evt) {
return detectMatched
}
return detectNotApplicable
case // release
webhook_module.HookEventRelease:
return matchReleaseEvent(payload.(*api.ReleasePayload), evt)
if matchReleaseEvent(payload.(*api.ReleasePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // registry_package
webhook_module.HookEventPackage:
return matchPackageEvent(payload.(*api.PackagePayload), evt)
if matchPackageEvent(payload.(*api.PackagePayload), evt) {
return detectMatched
}
return detectNotApplicable
case // workflow_run
webhook_module.HookEventWorkflowRun:
return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt)
if matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) {
return detectMatched
}
return detectNotApplicable
default:
log.Warn("unsupported event %q", triggedEvent)
return false
return detectNotApplicable
}
}
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool {
func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) detectResult {
// with no special filter parameters
if len(evt.Acts()) == 0 {
return true
return detectMatched
}
matchTimes := 0
@@ -393,14 +426,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
case "paths-ignore":
if refName.IsTag() {
@@ -410,14 +443,14 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
default:
log.Warn("push event unsupported condition %q", cond)
@@ -427,7 +460,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
if hasBranchFilter && hasTagFilter {
matchTimes++
}
return matchTimes == len(evt.Acts())
if matchTimes == len(evt.Acts()) {
return detectMatched
}
return detectFilteredOut
}
func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool {
@@ -478,7 +514,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
return matchTimes == len(evt.Acts())
}
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool {
func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) detectResult {
acts := evt.Acts()
activityTypeMatched := false
matchTimes := 0
@@ -525,7 +561,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
headCommit, err = gitRepo.GetCommit(prPayload.PullRequest.Head.Sha)
if err != nil {
log.Error("GetCommit [ref: %s]: %v", prPayload.PullRequest.Head.Sha, err)
return false
return detectNotApplicable
}
}
@@ -557,33 +593,39 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Skip(patterns, filesChanged) {
matchTimes++
}
case "paths-ignore":
filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err)
} else {
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
return detectNotApplicable
}
patterns, err := workflowpattern.CompilePatterns(vals...)
if err != nil {
break
}
if !workflowpattern.Filter(patterns, filesChanged) {
matchTimes++
}
default:
log.Warn("pull request event unsupported condition %q", cond)
}
}
return activityTypeMatched && matchTimes == len(evt.Acts())
if !activityTypeMatched {
return detectNotApplicable
}
if matchTimes != len(evt.Acts()) {
return detectFilteredOut
}
return detectMatched
}
func matchIssueCommentEvent(issueCommentPayload *api.IssueCommentPayload, evt *jobparser.Event) bool {
+29 -17
View File
@@ -101,49 +101,49 @@ func TestDetectMatched(t *testing.T) {
triggedEvent webhook_module.HookEventType
payload api.Payloader
yamlOn string
expected bool
expected detectResult
}{
{
desc: "HookEventCreate(create) matches GithubEventCreate(create)",
triggedEvent: webhook_module.HookEventCreate,
payload: nil,
yamlOn: "on: create",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventIssues(issues) `opened` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueOpened},
yamlOn: "on: issues",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventIssues(issues) `milestoned` action matches GithubEventIssues(issues)",
triggedEvent: webhook_module.HookEventIssues,
payload: &api.IssuePayload{Action: api.HookIssueMilestoned},
yamlOn: "on: issues",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestSync(pull_request_sync) matches GithubEventPullRequest(pull_request)",
triggedEvent: webhook_module.HookEventPullRequestSync,
payload: &api.PullRequestPayload{Action: api.HookIssueSynchronized},
yamlOn: "on: pull_request",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on: pull_request",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with no activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueClosed},
yamlOn: "on: pull_request",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `closed` action doesn't match GithubEventPullRequest(pull_request) with branches",
@@ -155,56 +155,56 @@ func TestDetectMatched(t *testing.T) {
},
},
yamlOn: "on:\n pull_request:\n branches: [main]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventPullRequest(pull_request) `label_updated` action matches GithubEventPullRequest(pull_request) with `label` activity type",
triggedEvent: webhook_module.HookEventPullRequest,
payload: &api.PullRequestPayload{Action: api.HookIssueLabelUpdated},
yamlOn: "on:\n pull_request:\n types: [labeled]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestReviewComment(pull_request_review_comment) matches GithubEventPullRequestReviewComment(pull_request_review_comment)",
triggedEvent: webhook_module.HookEventPullRequestReviewComment,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review_comment:\n types: [created]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPullRequestReviewRejected(pull_request_review_rejected) doesn't match GithubEventPullRequestReview(pull_request_review) with `dismissed` activity type (we don't support `dismissed` at present)",
triggedEvent: webhook_module.HookEventPullRequestReviewRejected,
payload: &api.PullRequestPayload{Action: api.HookIssueReviewed},
yamlOn: "on:\n pull_request_review:\n types: [dismissed]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventRelease(release) `published` action matches GithubEventRelease(release) with `published` activity type",
triggedEvent: webhook_module.HookEventRelease,
payload: &api.ReleasePayload{Action: api.HookReleasePublished},
yamlOn: "on:\n release:\n types: [published]",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventPackage(package) `created` action doesn't match GithubEventRegistryPackage(registry_package) with `updated` activity type",
triggedEvent: webhook_module.HookEventPackage,
payload: &api.PackagePayload{Action: api.HookPackageCreated},
yamlOn: "on:\n registry_package:\n types: [updated]",
expected: false,
expected: detectNotApplicable,
},
{
desc: "HookEventWiki(wiki) matches GithubEventGollum(gollum)",
triggedEvent: webhook_module.HookEventWiki,
payload: nil,
yamlOn: "on: gollum",
expected: true,
expected: detectMatched,
},
{
desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)",
triggedEvent: webhook_module.HookEventSchedule,
payload: nil,
yamlOn: "on: schedule",
expected: true,
expected: detectMatched,
},
{
desc: "push to tag matches workflow with paths condition (should skip paths check)",
@@ -222,7 +222,19 @@ func TestDetectMatched(t *testing.T) {
},
commit: nil,
yamlOn: "on:\n push:\n paths:\n - src/**",
expected: true,
expected: detectMatched,
},
{
desc: "push branch filter excludes -> filtered out",
triggedEvent: webhook_module.HookEventPush,
payload: &api.PushPayload{
Ref: "refs/heads/feature/x",
Before: "0000000",
Commits: []*api.PayloadCommit{{ID: "abc", Added: []string{"a.go"}, Message: "x"}},
},
commit: nil,
yamlOn: "on:\n push:\n branches: [main]",
expected: detectFilteredOut,
},
}
@@ -231,7 +243,7 @@ func TestDetectMatched(t *testing.T) {
evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn))
assert.NoError(t, err)
assert.Len(t, evts, 1)
assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
assert.Equal(t, tc.expected, detectWorkflowMatch(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0]))
})
}
}