Skip to content

Commit 283e641

Browse files
committed
ci: add github-actions-extended-ci workflow and maybe_stress job
This is a replacement for the TeamCity job `maybe_stress` (part of Bazel Extended CI) that runs on EngFlow instead of being `stress`'ed inside of the local TeamCity agent. We run each affected test 10 times and limit arbitrarily to 10 tests per package and 10 different packages (for now, we may decide to change this later). The code for `ci-stress` is adapted from `github-pull-request-make`, but rewritten substantially to make the code simpler. We don't simply use the existing code as a helper function so we can modify the new code without changing the existing code. Tests are stressed on EngFlow at a lower priority so as to not drastically impact the main CI jobs which must succeed to merge. Having said that, we still must monitor CI performance over the next few weeks to determine if we are safe to proceed. We also hope to add a `maybe_stressrace` build. Part of: DEVINF-1640 Epic: DEVINF-1582 Release note: none
1 parent aa92f9c commit 283e641

File tree

15 files changed

+374
-10
lines changed

15 files changed

+374
-10
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: GitHub Actions Extended CI
2+
on:
3+
pull_request:
4+
types: [opened, reopened, synchronize]
5+
branches:
6+
- "master"
7+
- "release-*"
8+
- "staging-*"
9+
- "!release-1.0*"
10+
- "!release-1.1*"
11+
- "!release-2.0*"
12+
- "!release-2.1*"
13+
- "!release-19.1*"
14+
- "!release-19.2*"
15+
- "!release-20.1*"
16+
- "!release-20.2*"
17+
- "!release-21.1*"
18+
- "!release-21.2*"
19+
- "!release-22.1*"
20+
- "!release-22.2*"
21+
- "!release-23.1*"
22+
- "!release-23.2*"
23+
- "!staging-v22.2*"
24+
- "!staging-v23.1*"
25+
- "!staging-v23.2*"
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
28+
cancel-in-progress: true
29+
jobs:
30+
maybe_stress:
31+
if: !github.event.pull_request.draft
32+
runs-on: [self-hosted, ubuntu_2004]
33+
timeout-minutes: 180
34+
steps:
35+
- uses: actions/checkout@v4
36+
with:
37+
ref: ${{ github.event.pull_request.head.sha || github.ref }}
38+
- name: Fetch the base branch (base ref)
39+
run: git fetch origin ${{ github.event.pull_request.base.ref }}:${{ github.event.pull_request.base.ref }}
40+
41+
- name: compute metadata
42+
run: echo GITHUB_ACTIONS_BRANCH=${{ github.event.pull_request.number || github.ref_name}} >> "$GITHUB_ENV"
43+
- run: ./build/github/get-engflow-keys.sh
44+
- run: ./build/github/prepare-summarize-build.sh
45+
- name: run tests
46+
run: ./build/github/maybe-stress.sh ${{ github.event.pull_request.base.ref }}
47+
- name: upload build results
48+
run: ./build/github/summarize-build.sh bes.bin
49+
if: always()
50+
env:
51+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
- name: clean up
53+
run: ./build/github/cleanup-engflow-keys.sh
54+
if: always()

build/github/maybe-stress.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
# Copyright 2025 The Cockroach Authors.
4+
#
5+
# Use of this software is governed by the CockroachDB Software License
6+
# included in the /LICENSE file.
7+
8+
set -euxo pipefail
9+
10+
# Usage: must provide a base branch as argument
11+
12+
if [ -z "$1" ]
13+
then
14+
echo 'Usage: build.sh BASEBRANCH'
15+
exit 1
16+
fi
17+
18+
BASEBRANCH="$1"
19+
20+
bazel build --config crosslinux //pkg/cmd/ci-stress \
21+
--jobs 50 $(./build/github/engflow-args.sh)
22+
23+
# NB: These jobs will run at a lower priority to ensure that the Essential CI
24+
# jobs (required to merge) will be minimally impacted.
25+
$(bazel info bazel-bin --config=crosslinux)/pkg/cmd/ci-stress/ci-stress_/ci-stress \
26+
$BASEBRANCH --config crosslinux --jobs 100 \
27+
--remote_execution_priority=-1 \
28+
--remote_download_minimal \
29+
--bes_keywords ci-stress --config=use_ci_timeouts \
30+
--build_event_binary_file=bes.bin \
31+
$(./build/github/engflow-args.sh)

pkg/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ ALL_TESTS = [
134134
"//pkg/cmd/bazci/githubpost:githubpost_test",
135135
"//pkg/cmd/bazci/testfilter:testfilter_test",
136136
"//pkg/cmd/bazci:bazci_lib_disallowed_imports_test",
137+
"//pkg/cmd/ci-stress:ci-stress_test",
137138
"//pkg/cmd/cmpconn:cmpconn_test",
138139
"//pkg/cmd/cockroach:cockroach_lib_disallowed_imports_test",
139140
"//pkg/cmd/dev:dev_lib_disallowed_imports_test",
@@ -1154,6 +1155,9 @@ GO_TARGETS = [
11541155
"//pkg/cmd/bazci/testfilter:testfilter_test",
11551156
"//pkg/cmd/bazci:bazci",
11561157
"//pkg/cmd/bazci:bazci_lib",
1158+
"//pkg/cmd/ci-stress:ci-stress",
1159+
"//pkg/cmd/ci-stress:ci-stress_lib",
1160+
"//pkg/cmd/ci-stress:ci-stress_test",
11571161
"//pkg/cmd/cmdutil:cmdutil",
11581162
"//pkg/cmd/cmp-protocol/pgconnect:pgconnect",
11591163
"//pkg/cmd/cmp-protocol:cmp-protocol",

pkg/cmd/ci-stress/BUILD.bazel

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
2+
3+
filegroup(
4+
name = "testdata",
5+
srcs = glob(["testdata/**"]),
6+
visibility = ["//pkg/cmd/github-pull-request-make:__pkg__"],
7+
)
8+
9+
go_library(
10+
name = "ci-stress_lib",
11+
srcs = ["main.go"],
12+
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/ci-stress",
13+
visibility = ["//visibility:private"],
14+
deps = ["//pkg/util/buildutil"],
15+
)
16+
17+
go_binary(
18+
name = "ci-stress",
19+
embed = [":ci-stress_lib"],
20+
visibility = ["//visibility:public"],
21+
)
22+
23+
go_test(
24+
name = "ci-stress_test",
25+
srcs = ["main_test.go"],
26+
data = [":testdata"], # keep
27+
embed = [":ci-stress_lib"],
28+
deps = ["//pkg/testutils/datapathutils"],
29+
)

pkg/cmd/ci-stress/main.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package main
7+
8+
import (
9+
"context"
10+
"errors"
11+
"fmt"
12+
"math/rand/v2"
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"regexp"
17+
"slices"
18+
"strings"
19+
20+
"github.com/cockroachdb/cockroach/pkg/util/buildutil"
21+
)
22+
23+
// It is a Test if there is a character after Test that is not a lower-case letter.
24+
const goTestStr = `func (Test[^a-z]\w*)\(.*\*testing\.TB?\) {$`
25+
26+
var currentGoTestRE = regexp.MustCompile(`.*` + goTestStr)
27+
28+
// getDiff returns the output of `git diff` from the given baseRef to the
29+
// current `HEAD`.
30+
func getDiff(ctx context.Context, baseRef string) (string, error) {
31+
cmd := exec.CommandContext(ctx, "git", "diff", "--no-ext-diff", fmt.Sprintf("%s..HEAD", baseRef))
32+
outputBytes, err := cmd.Output()
33+
if err != nil {
34+
return "", fmt.Errorf("unable to get diff: git diff %s..HEAD [...]: %w", baseRef, err)
35+
}
36+
return strings.TrimSpace(string(outputBytes)), nil
37+
}
38+
39+
// getPkgToTests parses a git-style diff and returns a mapping from directories
40+
// to affected tests in those directories in the given diff.
41+
func getPkgToTests(diff string) map[string][]string {
42+
const newFilePrefix = "+++ b/"
43+
44+
ret := make(map[string][]string)
45+
46+
var curPkgName string
47+
48+
for _, line := range strings.Split(diff, "\n") {
49+
if strings.HasPrefix(line, newFilePrefix) {
50+
if strings.HasSuffix(line, ".go") {
51+
curPkgName = filepath.Dir(strings.TrimPrefix(line, newFilePrefix))
52+
} else {
53+
curPkgName = ""
54+
}
55+
} else if currentGoTestRE.MatchString(line) && curPkgName != "" {
56+
curTestName := ""
57+
if !strings.HasPrefix(line, "-") {
58+
curTestName = currentGoTestRE.ReplaceAllString(line, "$1")
59+
}
60+
if curPkgName != "" && curTestName != "" {
61+
ret[curPkgName] = append(ret[curPkgName], curTestName)
62+
}
63+
}
64+
}
65+
66+
// Sanity-check: Make sure there is a `BUILD.bazel` file in each pkg,
67+
// or else it's not a real Go test. (Could be testdata for a different
68+
// package, etc.) Don't do this in test builds however, as we would
69+
// never find those files.
70+
//
71+
// We also take the opportunity to limit stressing to a constant
72+
// number of packages in case this PR changed a ton of packages
73+
// (find-and-replace, bulk changes, etc.)
74+
if !buildutil.CrdbTestBuild {
75+
taken := 0
76+
for pkg := range ret {
77+
_, err := os.Stat(filepath.Join(pkg, "BUILD.bazel"))
78+
if taken >= 10 {
79+
delete(ret, pkg)
80+
continue
81+
}
82+
if err != nil && errors.Is(err, os.ErrNotExist) {
83+
fmt.Printf("skipping testing package %s as we could not find a BUILD.bazel file in that directory\n", pkg)
84+
delete(ret, pkg)
85+
continue
86+
} else if err != nil {
87+
panic(err)
88+
}
89+
taken += 1
90+
}
91+
}
92+
93+
for _, tests := range ret {
94+
slices.Sort(tests)
95+
}
96+
// De-duplicate.
97+
for pkg := range ret {
98+
ret[pkg] = slices.Compact(ret[pkg])
99+
}
100+
101+
// We arbitrarily limit the number of tests per package. We randomize
102+
// the tests selected.
103+
for pkg := range ret {
104+
const maxTests = 10
105+
tests := ret[pkg]
106+
if len(tests) > maxTests {
107+
rand.Shuffle(len(tests), func(i, j int) {
108+
tests[i], tests[j] = tests[j], tests[i]
109+
})
110+
tests = tests[:maxTests]
111+
slices.Sort(tests)
112+
ret[pkg] = tests
113+
}
114+
}
115+
116+
return ret
117+
}
118+
119+
func runTests(ctx context.Context, pkgToTests map[string][]string, extraBazelArgs []string) error {
120+
var testPackages []string
121+
for pkg := range pkgToTests {
122+
testPackages = append(testPackages, fmt.Sprintf("//%s:%s_test", pkg, filepath.Base(pkg)))
123+
}
124+
allTests := make(map[string]struct{})
125+
for _, tests := range pkgToTests {
126+
for _, test := range tests {
127+
allTests[test] = struct{}{}
128+
}
129+
}
130+
allTestsSlice := make([]string, 0, len(allTests))
131+
for test := range allTests {
132+
allTestsSlice = append(allTestsSlice, test)
133+
}
134+
slices.Sort(allTestsSlice)
135+
testFilter := strings.Join(allTestsSlice, "|")
136+
testFilter = "^" + testFilter + "$"
137+
// Run each test multiple times.
138+
bazelArgs := []string{"test", "--test_filter", testFilter, "--runs_per_test", "10"}
139+
bazelArgs = append(bazelArgs, testPackages...)
140+
bazelArgs = append(bazelArgs, extraBazelArgs...)
141+
fmt.Printf("running `bazel` with args %+v\n", bazelArgs)
142+
cmd := exec.CommandContext(ctx, "bazel", bazelArgs...)
143+
cmd.Stdout = os.Stdout
144+
cmd.Stderr = os.Stderr
145+
return cmd.Run()
146+
}
147+
148+
func mainImpl(baseRef string, bazelArgs []string) error {
149+
ctx := context.Background()
150+
diff, err := getDiff(ctx, baseRef)
151+
if err != nil {
152+
return err
153+
}
154+
pkgToTests := getPkgToTests(diff)
155+
return runTests(ctx, pkgToTests, bazelArgs)
156+
}
157+
158+
func main() {
159+
if len(os.Args) == 1 {
160+
panic("expected at least one argument (the ref of the base brach)")
161+
}
162+
baseRef := os.Args[1]
163+
bazelArgs := os.Args[2:]
164+
if err := mainImpl(baseRef, bazelArgs); err != nil {
165+
panic(err)
166+
}
167+
}

pkg/cmd/ci-stress/main_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package main
7+
8+
import (
9+
"os"
10+
"reflect"
11+
"testing"
12+
13+
"github.com/cockroachdb/cockroach/pkg/testutils/datapathutils"
14+
)
15+
16+
func TestPkgToTests(t *testing.T) {
17+
for filename, expPkgs := range map[string]map[string][]string{
18+
datapathutils.TestDataPath(t, "skip.diff"): {
19+
"pkg/ccl/storageccl": []string{"TestPutS3"},
20+
},
21+
datapathutils.TestDataPath(t, "modified.diff"): {
22+
"pkg/crosscluster/physical": []string{"TestStreamingAutoReplan"},
23+
},
24+
datapathutils.TestDataPath(t, "removed.diff"): {},
25+
datapathutils.TestDataPath(t, "not_go.diff"): {},
26+
datapathutils.TestDataPath(t, "new_test.diff"): {
27+
"pkg/crosscluster/streamclient": []string{
28+
"TestExternalConnectionClient",
29+
"TestGetFirstActiveClientEmpty",
30+
},
31+
},
32+
datapathutils.TestDataPath(t, "27595.diff"): {
33+
"pkg/storage/closedts/container": []string{
34+
"TestTwoNodes",
35+
},
36+
"pkg/storage/closedts/minprop": []string{
37+
"TestTrackerConcurrentUse",
38+
},
39+
"pkg/storage/closedts/storage": []string{
40+
"TestConcurrent",
41+
},
42+
"pkg/storage/closedts/transport": []string{
43+
"TestTransportClientReceivesEntries",
44+
"TestTransportConnectOnRequest",
45+
},
46+
},
47+
} {
48+
t.Run(filename, func(t *testing.T) {
49+
content, err := os.ReadFile(filename)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
pkgToTests := getPkgToTests(string(content))
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
if !reflect.DeepEqual(pkgToTests, expPkgs) {
58+
t.Errorf("expected %s, got %s", expPkgs, pkgToTests)
59+
}
60+
})
61+
}
62+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)