Skip to content

Commit 9836924

Browse files
feat(ci): add 3.14/3.15 base images and scenario exclude for downstream gate
Add RE2-safe list-scenarios -exclude and ci.yml test_scenarios_exclude so wheel-only Python scenarios are not silently skipped. Ship Python 3.14/3.15 base images, dd-trace-py STS trigger policy, and migration gate index README. Scenario workloads land in follow-up PRs.
1 parent 4d9e5fa commit 9836924

10 files changed

Lines changed: 191 additions & 15 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Policy for: .github/workflows/prof-correctness.yml in DataDog/dd-trace-py
2+
# Allows dd-trace-py GitHub Actions to trigger prof-correctness workflows
3+
# when profiling code changes, to run correctness tests against just-built wheels.
4+
issuer: https://token.actions.githubusercontent.com
5+
subject_pattern: repo:DataDog/dd-trace-py:.*
6+
7+
claim_pattern:
8+
job_workflow_ref: DataDog/dd-trace-py/.github/workflows/prof-correctness.yml@refs/heads/.*
9+
ref_type: branch
10+
11+
permissions:
12+
actions: write

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,15 @@ jobs:
5858
python:
5959
uses: ./.github/workflows/test.yml
6060
with:
61-
test_scenarios: python.*
61+
test_scenarios: 'python.*'
62+
# Wheel-only scenarios need DDTRACE_INSTALL_URL (dd-trace-py downstream
63+
# gate), so they can't run here against PyPI ddtrace: every *_3.15.
64+
# python_downstream_gate is an index README, not a runnable scenario.
65+
# Go's regexp is RE2 (no negative lookahead), so this is an explicit
66+
# exclude rather than a lookahead baked into test_scenarios.
67+
# Additional wheel-only 3.14 dirs (e.g. live_heap) are added to this list
68+
# when those scenarios land.
69+
test_scenarios_exclude: '_3\.15$|^python_downstream_gate$'
6270
secrets: inherit
6371
full_host:
6472
uses: ./.github/workflows/test.yml

.github/workflows/test.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ on:
99
required: false
1010
type: string
1111
default: '.*'
12+
test_scenarios_exclude:
13+
description: 'A regexp dropping scenarios matched by test_scenarios (RE2, unanchored)'
14+
required: false
15+
type: string
16+
default: ''
1217
ddtrace_install_url:
1318
description: 'URL to a ddtrace install script (e.g. from S3 builds)'
1419
required: false
@@ -35,7 +40,7 @@ jobs:
3540
# - free local-daemon layer caching: buildBaseImages runs once per chunk,
3641
# and the chunk's scenarios reuse the built base image.
3742
run: |
38-
matrix=$(go run ./cmd/list-scenarios -pattern '${{ inputs.test_scenarios }}' -chunk-size 3)
43+
matrix=$(go run ./cmd/list-scenarios -pattern '${{ inputs.test_scenarios }}' -exclude '${{ inputs.test_scenarios_exclude }}' -chunk-size 3)
3944
echo "scenarios=$matrix" >> "$GITHUB_OUTPUT"
4045
4146
docker-scenarios:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ data/*
66
__pycache__/
77

88
.idea
9+
.vscode
910
gems.locked
1011

1112
.DS_Store

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ a step to analyze your results and match it against your expectation:
5858
You need to provide a JSON file with your expectations and a path to where to
5959
find the pprof files.
6060
61+
## Downstream from dd-trace-py
62+
63+
dd-trace-py triggers this repo after building wheels for a commit. The
64+
[`downstream-python.yml`](.github/workflows/downstream-python.yml) workflow
65+
installs ddtrace from the S3 wheel for that SHA (`DDTRACE_INSTALL_URL`) and
66+
runs selected Python scenarios.
67+
68+
**Triggers (non-blocking):**
69+
70+
| Source | When | Where to see results |
71+
|--------|------|----------------------|
72+
| **GitLab** `prof-correctness` job | After `upload all`; profiling path changes on `main` or MR | [prof-correctness Actions](https://github.com/DataDog/prof-correctness/actions/workflows/downstream-python.yml) — filter by commit SHA |
73+
| **GitHub** `.github/workflows/prof-correctness.yml` in dd-trace-py | Profiling path changes on PR/push; `workflow_dispatch` | Same Actions page |
74+
75+
Both use [dd-octo-sts](https://github.com/DataDog/dd-octo-sts-action) (`dd-trace-py.gitlab.trigger-ci` / `dd-trace-py.github.trigger-ci`) to call `gh workflow run downstream-python.yml`.
76+
77+
**Inputs:**
78+
79+
- `dd_trace_py_commit_sha` — commit to test (required)
80+
- `test_scenarios` — regexp passed to `TEST_SCENARIOS` (see the [3.14/3.15 migration gate index](scenarios/python_downstream_gate/README.md); the downstream workflow default alone is `python.*`)
81+
6182
## Creating new tests
6283

6384
### Define the dockerfile

base_images/Dockerfile.python-3.14

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM python:3.14 AS base
2+
3+
ARG DDTRACE_INSTALL_URL=""
4+
RUN if [ -n "$DDTRACE_INSTALL_URL" ]; then \
5+
curl -fsSL "$DDTRACE_INSTALL_URL" | bash; \
6+
fi
7+
8+
ENV DD_PROFILING_ENABLED=true
9+
ENV DD_TRACE_ENABLED=false
10+
ENV DD_PROFILING_OUTPUT_PPROF="/app/data/profiles"

base_images/Dockerfile.python-3.15

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM python:3.15.0b1 AS base
2+
3+
ARG DDTRACE_INSTALL_URL=""
4+
RUN if [ -n "$DDTRACE_INSTALL_URL" ]; then \
5+
curl -fsSL "$DDTRACE_INSTALL_URL" | bash; \
6+
fi
7+
8+
ENV DD_PROFILING_ENABLED=true
9+
ENV DD_TRACE_ENABLED=false
10+
ENV DD_PROFILING_OUTPUT_PPROF="/app/data/profiles"

cmd/list-scenarios/main.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Usage:
1414
//
1515
// go run ./cmd/list-scenarios -pattern 'python.*' -chunk-size 3
16+
// go run ./cmd/list-scenarios -pattern 'python.*' -exclude '_3\.15$' -chunk-size 3
1617
package main
1718

1819
import (
@@ -39,7 +40,7 @@ type matrixEntry struct {
3940
Names string `json:"names"`
4041
}
4142

42-
func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
43+
func run(pattern, exclude, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
4344
// Anchor the user pattern so e.g. "python" doesn't accidentally match
4445
// "python_basic_idle_3.12". The non-capturing group preserves precedence of
4546
// any alternation inside the user pattern.
@@ -48,21 +49,38 @@ func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
4849
return nil, fmt.Errorf("invalid -pattern: %w", err)
4950
}
5051

52+
// Optional exclusion, applied to names that matched -pattern. Unlike
53+
// -pattern this is NOT anchored, so a suffix like `_3\.15$` drops every
54+
// name ending in that version. Go's regexp is RE2 (no lookahead), so
55+
// "match python but not the wheel-only variants" must be expressed as a
56+
// separate exclude rather than a negative lookahead in -pattern.
57+
var excludeRe *regexp.Regexp
58+
if exclude != "" {
59+
excludeRe, err = regexp.Compile(exclude)
60+
if err != nil {
61+
return nil, fmt.Errorf("invalid -exclude: %w", err)
62+
}
63+
}
64+
5165
entries, err := os.ReadDir(scenariosDir)
5266
if err != nil {
5367
return nil, fmt.Errorf("read %s: %w", scenariosDir, err)
5468
}
5569

5670
var names []string
5771
for _, e := range entries {
58-
if e.IsDir() && re.MatchString(e.Name()) {
59-
names = append(names, e.Name())
72+
if !e.IsDir() || !re.MatchString(e.Name()) {
73+
continue
74+
}
75+
if excludeRe != nil && excludeRe.MatchString(e.Name()) {
76+
continue
6077
}
78+
names = append(names, e.Name())
6179
}
6280
sort.Strings(names)
6381

6482
if len(names) == 0 {
65-
return nil, fmt.Errorf("no scenarios matched pattern %q in %s", pattern, scenariosDir)
83+
return nil, fmt.Errorf("no scenarios matched pattern %q (exclude %q) in %s", pattern, exclude, scenariosDir)
6684
}
6785

6886
// Pack into chunks of at most chunkSize, preserving sorted order.
@@ -88,6 +106,7 @@ func run(pattern, scenariosDir string, chunkSize int) ([]matrixEntry, error) {
88106

89107
func main() {
90108
pattern := flag.String("pattern", "", "regex selecting scenario directory names (anchored as ^pattern$)")
109+
exclude := flag.String("exclude", "", "regex dropping matched names (unanchored, RE2); e.g. '_3\\.15$'")
91110
scenariosDir := flag.String("scenarios-dir", "scenarios", "path to the scenarios directory")
92111
chunkSize := flag.Int("chunk-size", 3, "max scenarios per matrix entry")
93112
flag.Parse()
@@ -103,7 +122,7 @@ func main() {
103122
}
104123

105124
abs, _ := filepath.Abs(*scenariosDir)
106-
out, err := run(*pattern, *scenariosDir, *chunkSize)
125+
out, err := run(*pattern, *exclude, *scenariosDir, *chunkSize)
107126
if err != nil {
108127
fmt.Fprintf(os.Stderr, "error: %v (resolved scenarios dir: %s)\n", err, abs)
109128
os.Exit(1)

cmd/list-scenarios/main_test.go

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"path/filepath"
77
"reflect"
88
"regexp"
9+
"strings"
910
"testing"
1011
)
1112

@@ -37,7 +38,7 @@ func TestRun_ChunksAlphabetically(t *testing.T) {
3738
"node_heap", // must be filtered out by the pattern
3839
})
3940

40-
got, err := run("python.*", root, 3)
41+
got, err := run("python.*", "", root, 3)
4142
if err != nil {
4243
t.Fatal(err)
4344
}
@@ -62,7 +63,7 @@ func TestRun_ChunksAlphabetically(t *testing.T) {
6263
func TestRun_SingleChunkWhenSmall(t *testing.T) {
6364
root := mkScenarios(t, []string{"dotnet_wall", "dotnet_alloc"})
6465

65-
got, err := run("dotnet.*", root, 3)
66+
got, err := run("dotnet.*", "", root, 3)
6667
if err != nil {
6768
t.Fatal(err)
6869
}
@@ -87,7 +88,7 @@ func TestRun_AnchoringRejectsSubstringMatches(t *testing.T) {
8788
"python_cpu_sleep_sync_3.12",
8889
})
8990

90-
got, err := run("python_cpu", root, 3)
91+
got, err := run("python_cpu", "", root, 3)
9192
if err != nil {
9293
t.Fatal(err)
9394
}
@@ -101,7 +102,7 @@ func TestRun_ExactChunkSizeBoundary(t *testing.T) {
101102
root := mkScenarios(t, []string{
102103
"a", "b", "c", "d", "e", "f",
103104
})
104-
got, err := run(".*", root, 3)
105+
got, err := run(".*", "", root, 3)
105106
if err != nil {
106107
t.Fatal(err)
107108
}
@@ -113,10 +114,46 @@ func TestRun_ExactChunkSizeBoundary(t *testing.T) {
113114
}
114115
}
115116

117+
func TestRun_ExcludeDropsMatchedNames(t *testing.T) {
118+
// Mirrors the CI python gate: run everything python except the wheel-only
119+
// variants (every *_3.15 plus python_live_heap_3.14).
120+
root := mkScenarios(t, []string{
121+
"python_cpu",
122+
"python_lock_3.14",
123+
"python_lock_3.15",
124+
"python_mem_domain_3.14",
125+
"python_mem_domain_3.15",
126+
"python_live_heap_3.14",
127+
"python_live_heap_3.15",
128+
})
129+
130+
got, err := run("python.*", `_3\.15$|^python_live_heap_3\.14$`, root, 3)
131+
if err != nil {
132+
t.Fatal(err)
133+
}
134+
135+
var names []string
136+
for _, e := range got {
137+
names = append(names, e.Names)
138+
}
139+
joined := strings.Join(names, ", ")
140+
want := "python_cpu, python_lock_3.14, python_mem_domain_3.14"
141+
if joined != want {
142+
t.Fatalf("excluded set wrong:\n got %q\nwant %q", joined, want)
143+
}
144+
}
145+
146+
func TestRun_InvalidExcludeIsError(t *testing.T) {
147+
root := mkScenarios(t, []string{"python_cpu"})
148+
if _, err := run("python.*", "[invalid", root, 3); err == nil {
149+
t.Fatal("expected error on invalid exclude regex")
150+
}
151+
}
152+
116153
func TestRun_NoMatchIsError(t *testing.T) {
117154
root := mkScenarios(t, []string{"python_cpu"})
118155

119-
_, err := run("ruby.*", root, 3)
156+
_, err := run("ruby.*", "", root, 3)
120157
if err == nil {
121158
t.Fatal("expected error when no scenarios match")
122159
}
@@ -125,7 +162,7 @@ func TestRun_NoMatchIsError(t *testing.T) {
125162
func TestRun_InvalidPatternIsError(t *testing.T) {
126163
root := mkScenarios(t, []string{"python_cpu"})
127164

128-
if _, err := run("[invalid", root, 3); err == nil {
165+
if _, err := run("[invalid", "", root, 3); err == nil {
129166
t.Fatal("expected error on invalid regex")
130167
}
131168
}
@@ -140,7 +177,7 @@ func TestRun_RegexMatchesIntendedDirAndOnlyThat(t *testing.T) {
140177
"python_cpu_sleep_sync_3.12",
141178
"python_basic_idle_3.12",
142179
})
143-
got, err := run("python.*", root, 3)
180+
got, err := run("python.*", "", root, 3)
144181
if err != nil {
145182
t.Fatal(err)
146183
}
@@ -175,7 +212,7 @@ func TestRun_RegexMatchesIntendedDirAndOnlyThat(t *testing.T) {
175212
// verifies it round-trips to the expected shape.
176213
func TestRun_OutputIsValidJSON(t *testing.T) {
177214
root := mkScenarios(t, []string{"a", "b", "c", "d"})
178-
got, err := run(".*", root, 3)
215+
got, err := run(".*", "", root, 3)
179216
if err != nil {
180217
t.Fatal(err)
181218
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Python downstream gate (dd-trace-py)
2+
3+
Paired **3.14 (baseline)** and **3.15 (candidate)** prof-correctness scenarios
4+
exercise the Python profiling stack for the 3.14 → 3.15 migration. They are the
5+
intended default set when dd-trace-py triggers downstream CI on profiling changes.
6+
7+
**Scenarios land in follow-up PRs** (core families first, then feature-specific
8+
pairs). This directory is an index only — not a runnable scenario.
9+
10+
## Scenarios
11+
12+
| Family | 3.14 (baseline) | 3.15 (candidate) | PR |
13+
|--------|-----------------|---------------------|-----|
14+
| _(pending)_ ||| Core scenarios in follow-up PRs |
15+
16+
## Default downstream regexp
17+
18+
Once scenarios are added, dd-trace-py should pass an explicit regexp (not the
19+
downstream workflow default of `python.*`). The regexp grows as families merge;
20+
see each PR for the current value.
21+
22+
Override via `workflow_dispatch``test_scenarios`, or when triggering
23+
[`downstream-python.yml`](../../.github/workflows/downstream-python.yml) manually.
24+
25+
## Wheel install
26+
27+
Every scenario builds against a **dd-trace-py wheel** via `DDTRACE_INSTALL_URL`
28+
(as `downstream-python.yml` does:
29+
`https://dd-trace-py-builds.s3.amazonaws.com/<sha>/install.sh`), pre-installed in
30+
the base image.
31+
32+
- **All `*_3.15` folders** — PyPI wheels may not be published for 3.15 yet;
33+
excluded from prof-correctness `main` CI (see `test_scenarios_exclude` in
34+
[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml)).
35+
- **Wheel-only 3.14 folders** — scenarios that depend on unreleased ddtrace
36+
features are also excluded from `main` CI until the feature ships.
37+
38+
## Local run
39+
40+
```sh
41+
export DDTRACE_INSTALL_URL="https://dd-trace-py-builds.s3.amazonaws.com/<commit-sha>/install.sh"
42+
TEST_SCENARIOS='<gate-regexp>' go test -v -run TestScenarios
43+
```
44+
45+
## Gate lifecycle
46+
47+
This gate tests the **migration delta** (3.14 → 3.15). It is time-boxed: retire
48+
the paired 14v15 framing at 3.15 GA and fold workloads into steady-state
49+
prof-correctness on {oldest, newest} supported Python versions.
50+
51+
## Further reading
52+
53+
- prof-correctness downstream wiring: [README](../../README.md#downstream-from-dd-trace-py)

0 commit comments

Comments
 (0)