Skip to content

Commit 7768028

Browse files
feat[rayci]: RunTagAnalysis and test (#321)
- Implements RunTagAnalysis function to determine which test tags to run based on changed files in a PR - Adds support for merging multiple tag rule config files with union of default tags - Includes comprehensive test coverage with a snapshot of ray-project/ray's production rules This is the Go equivalent of the Python determine_what_tests_to_run.py functionality. It enables conditional test execution by analyzing which files changed in a PR and mapping them to relevant test tags using the tag rule configuration files. Testing mimicks what exists today at https://github.com/ray-project/ray/blob/c963d646f0197947429b374cb06f831b47aab5dd/ci/pipeline/test_conditional_testing.py#L87 Includes a snapshot of ray-project/ray https://github.com/ray-project/ray/blob/62231dd4ba8e784da8800b248ad7616b8db92de7/ci/pipeline/test_rules.txt Topic: tagrulerun-go Relative: gitclient-go --------- Signed-off-by: andrew <andrew@anyscale.com>
1 parent 9940acc commit 7768028

File tree

3 files changed

+894
-0
lines changed

3 files changed

+894
-0
lines changed

raycicmd/tag_rule_run.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package raycicmd
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"sort"
8+
"strings"
9+
)
10+
11+
// mergedTagRuleConfig holds the result of merging multiple tag rule config files.
12+
type mergedTagRuleConfig struct {
13+
// RuleSet contains the merged rules for matching files to tags.
14+
RuleSet *TagRuleSet
15+
// DefaultTags contains the union of all default tags across all configs.
16+
// These are tags from default rules (\default).
17+
DefaultTags []string
18+
}
19+
20+
// loadAndMergeTagRuleConfigs loads and merges tag rule configurations from multiple files.
21+
// Tag definitions and rules from all files are combined into a single TagRuleSet.
22+
// Default tags (from \default rules) are unioned across all configs.
23+
func loadAndMergeTagRuleConfigs(configPaths []string) (*mergedTagRuleConfig, error) {
24+
merged := &mergedTagRuleConfig{
25+
RuleSet: &TagRuleSet{
26+
tagDefs: make(map[string]struct{}),
27+
},
28+
}
29+
30+
defaultTagSet := make(map[string]struct{})
31+
32+
for _, configPath := range configPaths {
33+
ruleContent, err := os.ReadFile(configPath)
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
cfg, err := ParseTagRuleConfig(string(ruleContent))
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
for _, tagDef := range cfg.TagDefs {
44+
merged.RuleSet.tagDefs[tagDef] = struct{}{}
45+
}
46+
merged.RuleSet.rules = append(merged.RuleSet.rules, cfg.Rules...)
47+
merged.RuleSet.defaultRules = append(merged.RuleSet.defaultRules, cfg.DefaultRules...)
48+
49+
// Collect default tags from default rules
50+
for _, rule := range cfg.DefaultRules {
51+
for _, tag := range rule.Tags {
52+
defaultTagSet[tag] = struct{}{}
53+
}
54+
}
55+
}
56+
57+
// Convert default tag set to sorted slice
58+
merged.DefaultTags = make([]string, 0, len(defaultTagSet))
59+
for tag := range defaultTagSet {
60+
merged.DefaultTags = append(merged.DefaultTags, tag)
61+
}
62+
sort.Strings(merged.DefaultTags)
63+
64+
return merged, nil
65+
}
66+
67+
// isPullRequest returns true if the current build is for a pull request.
68+
// Buildkite sets BUILDKITE_PULL_REQUEST to "false" for non-PR builds,
69+
// or to the PR number for PR builds.
70+
func isPullRequest(env Envs) bool {
71+
pr := getEnv(env, "BUILDKITE_PULL_REQUEST")
72+
return pr != "false" && pr != ""
73+
}
74+
75+
// needRunAllTags checks if all tags should be run regardless of changed files.
76+
// Returns true with a reason string if any of these conditions are met:
77+
// - RAYCI_RUN_ALL_TESTS=1 is set
78+
// - Building on the master branch
79+
// - Building on a release branch (releases/*)
80+
// - Not a pull request build
81+
func needRunAllTags(env Envs) (bool, string) {
82+
if getEnv(env, "RAYCI_RUN_ALL_TESTS") == "1" {
83+
return true, "RAYCI_RUN_ALL_TESTS is set"
84+
}
85+
86+
if !isPullRequest(env) {
87+
return true, "not a PR build"
88+
}
89+
90+
return false, ""
91+
}
92+
93+
// tagsForChangedFiles determines which tags to run based on changed files.
94+
// For each file, rules are evaluated in order:
95+
// - If a rule matches, its tags are added to the result
96+
// - If the rule has Fallthrough=true, continue to the next rule
97+
// - Otherwise, stop processing rules for this file
98+
//
99+
// If no non-fallthrough rule matches a file, default rules are applied.
100+
// Fallthrough rules add tags but don't prevent default rule fallback.
101+
func tagsForChangedFiles(ruleSet *TagRuleSet, files []string) []string {
102+
tagSet := make(map[string]struct{})
103+
104+
for _, file := range files {
105+
terminatingRuleMatched := false
106+
for _, rule := range ruleSet.rules {
107+
if rule.Match(file) {
108+
for _, tag := range rule.Tags {
109+
tagSet[tag] = struct{}{}
110+
}
111+
if !rule.Fallthrough {
112+
terminatingRuleMatched = true
113+
break // stop processing rules for this file
114+
}
115+
}
116+
}
117+
// If no terminating rule matched, apply default rules from all configs
118+
if !terminatingRuleMatched {
119+
if len(ruleSet.defaultRules) > 0 {
120+
for _, rule := range ruleSet.defaultRules {
121+
for _, tag := range rule.Tags {
122+
tagSet[tag] = struct{}{}
123+
}
124+
}
125+
} else {
126+
log.Printf("unhandled file (no matching rule): %s", file)
127+
}
128+
}
129+
}
130+
131+
tags := make([]string, 0, len(tagSet))
132+
for tag := range tagSet {
133+
tags = append(tags, tag)
134+
}
135+
sort.Strings(tags)
136+
return tags
137+
}
138+
139+
// RunTagAnalysis determines which test tags to run based on changed files.
140+
//
141+
// For PR builds, it analyzes changed files against tag rules and returns
142+
// only the relevant tags. For non-PR builds (master, release branches, etc.),
143+
// it returns ["*"] to run all tags.
144+
//
145+
// The function requires BUILDKITE=true and, for PR builds, also requires
146+
// BUILDKITE_PULL_REQUEST_BASE_BRANCH and BUILDKITE_COMMIT to be set.
147+
func RunTagAnalysis(
148+
configPaths []string,
149+
env Envs,
150+
lister ChangeLister,
151+
) ([]string, error) {
152+
if getEnv(env, "BUILDKITE") != "true" {
153+
return nil, fmt.Errorf("BUILDKITE environment variable is not set")
154+
}
155+
156+
runAll, reason := needRunAllTags(env)
157+
if runAll {
158+
log.Printf("running all tags: %s", reason)
159+
return []string{"*"}, nil
160+
}
161+
162+
// If no config files exist, run all tags. This matches the original
163+
// Python behavior when ci/pipeline/test_conditional_testing.py was absent.
164+
// See: https://github.com/ray-project/rayci/blob/23e47c9b5502a3646f506cf5362c6d3507952bce/raycicmd/step_filter.go#L108-L109
165+
hasConfigFile := false
166+
for _, configPath := range configPaths {
167+
if _, err := os.Stat(configPath); err == nil {
168+
hasConfigFile = true
169+
break
170+
}
171+
log.Printf("config file not found: %s", configPath)
172+
}
173+
if !hasConfigFile {
174+
log.Printf("no config files found, running all tags")
175+
return []string{"*"}, nil
176+
}
177+
178+
baseBranch := getEnv(env, "BUILDKITE_PULL_REQUEST_BASE_BRANCH")
179+
commit := getEnv(env, "BUILDKITE_COMMIT")
180+
if baseBranch == "" || commit == "" {
181+
return nil, fmt.Errorf(
182+
"BUILDKITE_PULL_REQUEST_BASE_BRANCH and BUILDKITE_COMMIT are required for PR builds",
183+
)
184+
}
185+
186+
merged, err := loadAndMergeTagRuleConfigs(configPaths)
187+
if err != nil {
188+
return nil, fmt.Errorf("load tag rules: %w", err)
189+
}
190+
191+
changedFiles, err := lister.ListChangedFiles()
192+
if err != nil {
193+
return nil, fmt.Errorf("list changed files: %w", err)
194+
}
195+
196+
log.Printf("base branch: %s, commit: %s", baseBranch, commit)
197+
log.Printf("changed files: %v", changedFiles)
198+
199+
tags := tagsForChangedFiles(merged.RuleSet, changedFiles)
200+
log.Printf("selected tags: %s", strings.Join(tags, " "))
201+
202+
return tags, nil
203+
}

0 commit comments

Comments
 (0)