Repository navigation
ci: classify pull request changes as infra or product - #820
Merged
Merged
Conversation
Add a path classifier that decides whether a pull request touches the proxy or only tooling around it. Two ordered tables of fnmatch globs (product overrides first, then the infra rules) give every path a class and a rule id; anything that matches no rule is product, and so is an empty or unreadable file list. The CLI reads changed paths from a file or stdin, appends product=true|false to the GitHub output file, optionally writes a markdown table of path, class and rule to the step summary, and reports product=true on any error instead of failing. Add a reusable change-scope workflow that wraps it. It reports product for every event other than a pull request and when the pull request has the ci-full label (read through the API at run time so a re-run picks up a label added later). Otherwise it checks out the merge ref, lists the net change against the base with renames split into old and new paths, and classifies it. Any failure in that path reports product. Nothing calls the workflow yet. The classifier tests run in the changelog PR workflow. They cover each rule, the overrides, the fail-safe cases, the file lists of real pull requests, every tracked file, a dead-rule guard and the CLI.
Contributor
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
|
This was referenced Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a path classifier that decides whether a pull request changes the proxy (product) or only the tooling around it (infra), and a reusable
change-scopeworkflow that exposes the answer as an output. Nothing calls the workflow yet, so this PR changes no CI behaviour. It lands the classifier, its tests and the workflow so later PRs can gate jobs onneeds.scope.outputs.product.The classifier is fail-safe by construction: a path that matches no rule is product, and so is an empty or unreadable file list, a crash, an unknown argument, or any event other than
pull_request.Changes
scripts/ci/change_scope.py(new, stdlib only).classify(path) -> (is_product, rule_id)andscope(paths) -> bool. Two ordered glob tables,PRODUCT_OVERRIDES(P1 to P4) thenNON_PRODUCT(I1 to I12); the first match wins and everything else isP-default. Paths that are absolute, contain.., or are blank are product. CLI:--files-from FILE|-,--force-product,--github-output FILE(appendsproduct=true|false; prints it to stdout when absent),--summary FILE(markdown table of path, class, rule, capped at 500 rows). On empty input or any error it reportsproduct=trueand exits 0.scripts/ci/tests/test_change_scope.py(new, 35 tests). Per-rule table, overrides beatingscripts/**andsecurity/**, unknown, empty and mixed lists, the file lists of 13 real pull requests as literal fixtures, every tracked file classifies, a dead-rule guard (every glob matches at least one tracked file), and the CLI (output format, append behaviour,--force-product, empty and missing input, unknown argument, unwritable summary)..github/workflows/change-scope.yaml(new,on: workflow_call, outputproduct). Reportstruefor any event other thanpull_requestand when the PR has theci-fulllabel, read through the API at run time so a re-run after adding the label picks it up. Otherwise it checks out the merge ref (fetch-depth: 2,persist-credentials: false), runsgit diff --name-only --no-renames HEAD^1 HEAD, and pipes that into the classifier.--no-renameslists both sides of a rename, so moving a file out of a product directory into an infra one is still seen. The gate, checkout and classify steps arecontinue-on-error, and a final step reportsfalseonly when every step succeeded and the classifier saidfalse; every other outcome istrue. Values reachrun:throughenv:only. Permissions: workflow{}, jobcontents: readandpull-requests: read. Callers must grant the same..github/workflows/changelog-pr.yaml: addsscripts.ci.tests.test_change_scopeto the existing unittest step.Classification
scripts/ci/run_security_regressions.sh,scripts/ci/run_zap_scan.sh,security/zap/**scripts/ci/wait_e2e_stack.sh,scripts/ci/wait_loki_track_stack.sh,scripts/ci/wait_pipeline_stack.sh,scripts/smoke-test.sh,scripts/compat/**scripts/ci/collect_quality_metrics.sh,scripts/ci/check_quality_gate.py,scripts/ci/render_pr_report.sh,scripts/ci/check_conformance.pyscripts/ci/change_scope.py,.github/workflows/change-scope.yaml.github/**scripts/**security/**bench/ab/history/**,bench/ab/results/**bench/is product.test/*/docker-compose*.ymltest/e2e-ui/package.json,test/e2e-ui/package-lock.jsonexamples/**,docker-compose.ymlwebsite/**docs/**,results/**README.md,CHANGELOG.md,LICENSE,SECURITY.md,CONTRIBUTING.mdconformance/registry/generated/perf-evidence.json,conformance/reports/performance.md,conformance/reports/gaps.md.gitignore,.gitleaks.toml,.trivyignore.yaml,.hadolint.yaml,.golangci.ymlcmd/,internal/,charts/,go.mod,Dockerfile, the rest ofconformance/,test/andbench/.Matching is segment-aware:
*and?never cross/,**does. So.github/**matches every nested path, whiletest/*/docker-compose*.ymlmatches only a compose file one directory belowtest/(a deeper one stays product).Proofs
Real pull request file lists (
gh api repos/.../pulls/N/files) through the CLI:Over
git ls-files(1807 files): 510 infra, 1297 product. Infra by rule: I1 29, I2 25, I3 6, I4 177, I5 6, I6 2, I7 30, I8 34, I9 188, I10 5, I11 3, I12 5. Product by rule: P1 3, P2 5, P3 4, P4 2, P-default 1283. No glob is dead and the six compose files undertest/are the only I5 matches.Merge-ref check in a scratch repository: a PR that renames
internal/a.gotodocs/a.go. With--no-renamesthe list contains both paths and the result isproduct=true; with rename detection onlydocs/a.gowould be listed.Not applicable: infra-only PR; product checks (e2e vs Loki, Grafana range matrix, visual, A/B, benchstat, load) do not apply.