Repository navigation
ci: a CI-only pull request runs only the jobs that verify what it changes; a big change runs a full baseline - #895
Conversation
⚪ Performance A/B: no change beyond noise8 shape×range — 8 ⚪ same · results vs Loki: 7 same, 0 new difference(s), 1 pre-existing Every shape is within noise of the base build (±30% and 100 ms). 8 unchanged shape×range (within noise)
Cold (first-run) timings — what a user opening a panel sees
Why these shapes
base |
✅ Visual smoke: passed with warningsBase
Gate: a base-vs-PR data difference passes when the PR is closer to Loki, and fails when the PR diverges from Loki or Loki cannot decide it (at a range Loki does not hold, a difference passes when the same page proved it closer to Loki at a shorter range; otherwise label the pull request dd-fields-1h: history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-28T09%3A21%3
dd-service-logs-1h: history-dependent difference, not gated (1): resource /api/datasources/uid/*/resources/patterns?end=2026-10-28T09%3A21%3
All montages of this run: |
PR Quality ReportCompared against base branch Coverage and tests
Compatibility
Performance smokePerformance smoke was skipped for this PR because no perf-sensitive paths changed. State
|
…nges Change scope knew two answers, product and infra, and a path with no rule was product. Four CI-only pull requests of 2026-10-28 (#888 Loki stack configs, #889 the A/B harness, #891 a README next to a workflow edit, #892 the quality report script) therefore ran the whole product CI, about 200 runner-minutes each. change_scope.py now sorts every path into four nested lanes and a pull request runs the jobs of the highest lane any path needs: infra nothing beyond lint, helm, Security / static and CodeQL perf + perf-smoke (bench/ab, except the modules bench/visual, bench/parity and the conformance gate read: P5) stack + every job that starts a compose stack (the CI compose files and the configs the e2e-compat stack mounts) product + the Go unit tests, race, fuzz, bench, conformance, report Markdown at the root and under bench/ and test/ is infra; Markdown under conformance/, charts/, cmd/, internal/, pkg/, dashboard/ and alerting/ is still product because a gate reads it or it ships. The two benchmark Loki configs are infra; the three CI compose files move from infra to the stack lane, the three compose files no job starts stay infra. P1 to P4 keep their meaning, an unknown path, an empty list and any error are still product. change-scope.yaml reports product, stack and perf as nested outputs. ci.yaml gates image on stack; tuple-smoke, e2e-compat, e2e-fleet and e2e-ui keep following test but also run when test was skipped for a stack-only change. The e2e-compat aggregator accepts skipped groups only when both lanes are false. The compat, pipeline, security-runtime, visual and perf workflows gate on their lane. The bench module tests (the drift guard of the three Loki configs) and the bench/ab unit tests move from test and conformance into lint, so the lanes that skip the Go jobs still run them. Tests: table cases for the five pull requests, a pure Go change, every tracked path that ships or sits under a Go, chart or image root (must be product), a per-workflow gate-to-lane table and mutation cases for the new gates.
…make a new baseline A pull request into main whose diff in shipped paths is big, or that carries the baseline label, now runs the heavy set instead of the selection-based smoke: every A/B shape of every set (selection.py --all, at the smoke's ranges), every visual page (plan.py --all) and the whole VictoriaLogs version matrix (vl-matrix, until now weekly only). An ordinary product pull request keeps the selection. Trigger (change_scope.py, only for a pull request whose base is main): more than 1500 changed lines or more than 40 files in ships() paths, or at least 150 changed lines in the files every query passes through (the route table, the per-route wrapper and translation entry, the request scope, the cache keys, the middleware and translator.go). On the merged pull requests #678 to #894 that selects 6 of the 45 that changed shipped code (13%), 1 of 6 in the last 40; counting any edit of those files would select 44%. The baseline label forces it and the full product CI. ci-full does not: it runs the normal CI on a change that touches no product path, where a baseline would only repeat main's. A pull request that is not based on main never gets it (ci, compat-vl, perf-ab and visual-smoke trigger on branches: [main] only). Each layer of a stack is judged on its own diff once it targets main, so a refactor split into small layers does not trigger; the label on the last layer does. Unlike the lanes, the baseline output fails towards false: an error, an unreadable label list or an empty diff report false. The results are never committed. baseline_candidate.py writes, from the run's summaries, the above-2x Loki list as it would become (keeping the committed rows of ranges the run did not measure), which a person can copy over history/over-2x-loki.json, and a report of the run's acceptance distribution (history/acceptance.json is rebuilt from the daily history, so the next daily run writes the real one). The jobs upload them and the visual captures as artifacts and end their comments with a "new baseline candidate" section. A test checks that no job that reads the baseline output can commit, push or open a pull request. Tests: threshold boundaries, a big refactor, a small fix, a stacked pull request, the label, an infra pull request with only the label, ci-full alone, recorded pull requests, the CLI, selection.select_all, plan.plan_all, the candidate files and comment, pr_smoke with the flag, and the wiring of the three workflows.
…ine comment strings explicitly
67837cc to
8934ba0
Compare





ci: a CI-only pull request runs only the jobs that verify what it changes, and a very large change re-measures the baseline once
Four recent CI-only pull requests ran the whole product CI (about 200 runner-minutes each) because change scope knew two answers, product and infra, and a path with no rule counted as product:
test/e2e-compat/loki-*-config.yamlbench/ab/*.py, its tests, fixtures, READMEtest/e2e-ui/README.md(plus docs, workflow)scripts/ci/collect_quality_metrics.shscripts/ciWhat changes
scripts/ci/change_scope.pysorts every path into one of four nested lanes. A pull request runs the jobs of the highest lane any path needs. lint, helm, Security / static and CodeQL run in every lane..github/,scripts/,docs/,website/,examples/, Markdown at the root and underbench/andtest/, the two benchmark Loki configs, the compose files no job startsbench/ab/**exceptstack.py,selection.py,shapes.jsonand the compose overlays (read bybench/visual,bench/parityand the conformance gate: rule P5, product)e2e-compat,e2e-fleet,e2e-pipeline) and the configs the e2e-compat stack mounts (loki-local-config.yaml,grafana-*.yaml,vm*.yml,nginx-tail.conf)ci-fulllabel, non-PR events, P1 to P4change-scope.yamlreports three cumulative outputs,product,stackandperf(productimpliesstackimpliesperf). A job is skipped only on an explicitfalsefrom a classifier that ran; anything else keeps every lane on. Skipped jobs report as skipped, which the required checks accept, exactly as the infra lane already did.Wiring (
ci.yaml):imagegates onstack.tuple-smoke,e2e-compat-group,e2e-fleetande2e-uikeep followingtest, but also run whentestwas skipped because only a stack path changed (needs.test.result == 'skipped'withproduct == 'false'andstack == 'true', compared with==so an empty output cannot take that route). Thee2e-compataggregator accepts skipped groups only when both lanes sayfalse. The other workflows gate their stack jobs onstackand perf-smoke onperf;reportandrace-stressstay onproduct.Two checks moved so that the lanes that skip the Go unit tests still run them, with no change for product pull requests: the bench module's vet and tests (7 s; they hold the drift guard between
loki-local-config.yamland the two benchmark configs) and thebench/abunit tests (2 s) now run in lint instead oftestandconformance.Markdown is infra only where nothing reads it as data: not under
conformance/(the generated reports are compared by the conformance gate),charts/(the chart README ships and the flag documentation gate reads it),cmd/,internal/,pkg/,dashboard/oralerting/.Compose files that a job starts moved from infra to the stack lane, so a pull request that edits only
test/e2e-compat/docker-compose.yml,test/e2e-fleet/docker-compose.ymlortest/e2e-pipeline/docker-compose.ymlnow runs the stack jobs it used to skip (about 120 runner-minutes more for such a pull request). That is deliberate: nothing in pull request CI checked a compose edit against the stack before, and e2e-compatcoreincludesTestPinnedCompatibilityMatrixMatchesCompose, which compares the compose file with the pinned compatibility matrix. The three compose files no job starts (docker-compose.bench.yml,docker-compose.review.yml,docker-compose.http-discovery.yml) stay infra.Proof that no product pull request loses a job
LaneRepositoryTests: every tracked path withships()true, every tracked path undercmd/,internal/,pkg/,charts/,dashboard/,alerting/andgo.mod,go.sum,Dockerfile,.dockerignore, every tracked path underconformance/except the three generated reports already infra, and every file ofbench/visual,bench/parity,bench/cmdandbench/internalclassify as the full product lane.bench/abfiles to perf, 8 stack configs to stack, 7 Markdown files and 2 benchmark configs to infra) and 3 to a larger one (the three CI compose files, infra to stack, deliberate).test_scope_wiring.pychecks every gate of every workflow against its lane and has mutation cases for a stack job that runs after a failedtest, for the skipped-testroute on an empty output, a missingscopeinneeds, an aggregator that ignoresstack, and an image gated on the wrong lane.Baseline run for very large changes (second commit)
A very large change to the proxy now re-measures everything once, so later pull requests are compared with a fresh baseline. It is never run on every pull request: an ordinary product pull request keeps today's selection-based perf-smoke and visual-smoke.
A baseline run replaces the selection with:
bench/ab/shapes.json(240 shapes in 25 sets) at the smoke's ranges (selection.py --all);plan.py --all);vl-matrix, the five supported versions), run on the pull request instead of only the weekly schedule.Trigger (
scripts/ci/change_scope.py, constantsBASELINE_*, only for a pull request whose base ismain):baselinelabel, which also forces the full product CI; orships(): tests and testdata do not count) has more than 1,500 changed lines, or more than 40 files, or at least 150 changed lines in the files every query passes through:internal/proxy/{proxy.go, query_translation.go, request_scope.go, cache_keys.go, middleware.go}andinternal/translator/translator.go(route table, per-route wrapper and translation entry, tenant and routing scope, canonical cache keys, tenant validation and security headers, LogQL to LogsQL translation). The entry-point rule has a size because counting any edit would select 44% of the pull requests that change shipped code.Both the
baselineand theci-fulllabels force it (with the full product CI). A label never forces it on a stacked pull request: only a pull request intomaincan get it. Thebaselinelabel exists on the repository.Stacked pull requests never run it.
ci.yaml,compat-vl.yaml,perf-ab.yamlandvisual-smoke.yamlall trigger onpull_request: branches: [main], so a pull request based on another branch starts none of them; the classifier also refuses any base other thanmain, label or not (tests:test_heavy_workflows_only_run_for_pull_requests_into_main,test_a_stacked_pull_request_never_gets_it). Each layer is judged on its own diff once it targetsmain(retargeting firesedited, which these workflows do not listen for, so the run starts with the next push or a re-run). A big change split into layers that are each under the thresholds does not trigger; several big layers trigger once each. Put thebaselinelabel on the last layer of a split refactor. Thebaselineoutput fails the other way from the lanes: an error, an unreadable label list, an empty diff or a non-PR event all reportfalse.Results are never committed:
pr_smoke.py --baseline-candidate DIRwritesover-2x-loki.json(the committed list with the set x range pairs this run measured replaced by this run's rows, the 24h and 7d rows stay),acceptance-day.json(one day in the shape of an entry ofhistory/acceptance.json: share below Loki, close, 1-2x, above 2x per mode),candidate.mdandmeta.json, uploaded as theperf-baseline-candidateartifact. The sticky A/B comment ends with a "New baseline candidate" section: the distribution next to the committed day's, and the above-2x rows added and no longer above.visual-baseline-candidate; the visual comment ends with a fixed "New baseline candidate" section pointing at it (rendered by the base branch's trusted job, no pull request data).over-2x-loki.jsonis a proposed replacement: a person copies it overbench/ab/history/over-2x-loki.jsonin a pull request (check_conformance.pychecks that list against the registry locks; the daily run keeps it from there).acceptance-day.jsonis a report and is not copied:history/acceptance.jsonis rebuilt fromhistory/*.jsonlby every daily run, so the real new distribution is the next daily run's, which runs whenever a measured path changed on main; the run on the pull request is its preview. The visual captures are a report to read. The jobs hold no write token for any of it, and a test checks that no job that reads the baseline output can commit, push or open a pull request.Trigger rate (replay of the merged pull requests #678 to #894, file lists and line counts from the API; "changed shipped code" leaves out the release-metadata, dependency and daily-report bots):
Read against the pull requests that changed shipped code. Against the whole product lane it is 4.1%: product-lane pull requests that ship nothing (gates, registry, bench, conformance) can never trigger by size, so that denominator cannot reach 10 to 20% without firing on most real code pull requests.
The six: #862 (1,739 lines, 222 in the entry points), #846 (299 entry-point lines), #760 (1,710 lines), #747 (333 entry-point lines), #739 (282 entry-point lines), #704 (1,565 lines). Near misses: #758 (1,090 lines), #746 (1,364 lines, 10 files). Lines and files alone (the owner's proposal) select 3 of the 45 (7%); the entry-point rule brings it to 13%. None of the 200 would have run it on a non-main base or outside the product lane.
Cost of a baseline run, from the last full runs (perf-smoke 171 shape x range rows in 14.5 min plus 3 min of setup; visual-smoke 37 captures in 13.7 min including about 5 min of setup; vl-matrix 5 x 6.3 min):
So a baseline pull request costs about 300 runner-minutes instead of about 210. These are estimates from the logs above; the first baseline run gives the measured number. Job timeouts are 90 minutes for perf and visual.
Setup before use: create the
baselinelabel on GitHub (this change does not create it). Labels are read when the workflows run, so add it and then push or re-run the checks.Estimated runner-minutes saved per pull request
From the job durations of one green full run (about 209 min with perf-smoke, 193 min when perf-smoke selects nothing; always-on jobs about 8 min):
bench/ortest/bench/abPython changebench/abtests, fixtures or README onlyloki-local-config.yaml,grafana-*.yaml,vm*.yml,nginx-tail.conf)The e2e-ui time comes from the nine-shard layout that preceded #891, so the stack figure is slightly high on both sides.
Not changed
P1 to P4 keep their meaning; an unknown path is still product; editing the classifier or its workflow still runs everything. #892-type changes (P3) still run the full CI.