Pull request CI scope¶
Lint's python-unit-tests job discovers tests/ on every pull request and every
push to main. On a pull request, scripts/ci/pr_unit_test_selector.py may
replace that discovery with an explicit unittest module list. The map is
scripts/ci/pr-unit-test-map.yaml. Pushes to main do not call the selector.
Three other Lint jobs can fast-success on a pull request when none of their
inputs changed. The job still finishes as success. It does not skip the job.
ci-gate still requires success for every needed job, and a skipped job would
fail that gate.
The dependency-update contract step still runs
tests.test_dependency_update_contracts and tests.test_oneuptime_env_render
on every Lint run, before the selector.
Decision¶
The selector reads the pull-request files API with pagination and compares the
collected paths to the pull request's changed_files count. It prints full
or one tests.* module per line. The job writes that decision to the GitHub
step summary.
Full discovery (python -m unittest discover -s tests -v) is required when any
of these are true:
- The event is not
pull_request, including every push tomain. - The files API errors, the token is missing, or a page does not add up to
changed_files. - Any changed path is unmapped, or the module list would be empty.
- A changed path matches a force-full pattern:
.github/**, root and nestedpyproject.toml,uv.lock,requirements*.txt,package.json,package-lock.json,.pre-commit-config.yaml,inventory/generators/**,inventory/schema/**,AGENTS.md,.cursor/**,PLAN.md, orREADME.md. - The selector or its map is in the diff.
- A path under
tests/was removed or renamed. - The selector crashes or prints something that is not
fullor a module list. The job says so in the step summary and still runs full discovery. A crash is not a successful narrow run.
When every path is mapped, the module list is the union of those rules. A rule
marked service_backup_scanners also adds the three tests that read
services/*/backup.yml.
flowchart TD
start[python-unit-tests] --> contracts[contract modules always run]
contracts --> kind{pull_request?}
kind -->|no| discover[unittest discover -s tests]
kind -->|yes| api[paginated pulls files API]
api -->|error, short page, or crash| discover
api --> mapped{every path mapped?}
mapped -->|no| discover
mapped -->|yes| narrow[union of mapped modules]
Mapped prefixes¶
A docs path that no other test reads uses only the docs allowlist. A docs path named in the map also runs the tests that read that file.
| Changed path | Modules beyond the docs allowlist |
|---|---|
docs/**, mkdocs.yml, .markdownlint-cli2.yaml, tests/test_docs.py, tests/test_documentation_remediation.py |
none; tests.test_docs and tests.test_documentation_remediation |
docs/runbooks/restore.md and the other WS2 render runbooks in the map |
tests.test_ws2_secret_procedures, tests.test_ws2_secret_rendering |
| Komodo webhook, LXC, socket, and project-map paths in the map | the WS5 modules and tests.test_explicit_deployment_review227682da_contracts that read them |
| WS4, WS6, WS7, runner, and OneUptime dispatch docs named in the map | the single contract module that reads that file |
services/monitoring-canary/** |
tests.test_monitoring_canary plus tests.test_backup_freshness_incidents, tests.test_backup_snapshot_contract, and tests.test_restore_health_incidents |
scripts/hotfix-syno-capacity.sh |
tests.test_synology_capacity_policy |
scripts/render-alertmanager-config.py |
tests.test_alertmanager_render |
scripts/render-prometheus-validation-config.py |
tests.test_prometheus_validation_render |
scripts/network-scan.py |
tests.test_network_scan |
scripts/check-docs.py |
tests.test_docs |
scripts/check-documentation-remediation.py |
tests.test_documentation_remediation |
scripts/lint-alert-schema.py |
tests.test_alert_schema and tests.test_ws4_monitoring_contracts |
A pull request that changes two mapped prefixes runs the union. docs/** plus
services/monitoring-canary/** is that union. docs/** plus an unmapped
service such as services/litellm/** is full discovery.
Still full¶
These areas stay unmapped. A diff that includes one of them runs the whole suite:
- Almost every
services/<name>/tree other thanmonitoring-canary. LiteLLM, Komodo, Paperless, Orders, Reddit Explorer, and the image-update cohorts are in this set. Their tests share compose files, backup manifests, Komodo resource checks, and runbook readers, and several of those reads are loops rather than one literal path. infra/ansible/**, inventory YAML outside the force-full generator and schema trees,monitoring/**,backups/**,reviews/**, andscripts/**other than the scripts in the table.- Any new test module that is not itself a mapped test path. Adding, editing, renaming, or deleting it is full discovery unless the map names that file.
test_docs.py reads mkdocs.yml and two docs pages, and the docs checker it
imports walks services, inventory, workflows, and generator entry points when
the documentation job runs. That is why the policy files above stay full even
for a docs-heavy pull request. The documentation job still runs
scripts/check-docs.py and the MkDocs build on every Lint run. See
Documentation validation.
What the unit-test selector does not change¶
ci-gate still requires every existing Lint job to finish as success.
phoenix-api-routing stays outside the gate. Deploy workflows are unchanged.
Runner labels and CI_RUNS_ON / DOCKER_RUNS_ON are unchanged. Security keeps
its own gate and has no paths: filter. Lint has no paths: filter either:
fast-success is a step inside the job, not a skipped workflow.
Fast-success jobs¶
On a pull request, scripts/ci/pr_job_scope.py reads the same paginated pulls
files list as the unit-test selector. The workflow wrapper is
scripts/ci/pr_job_scope_step.sh. Stdout must be the single token fast or
run. The reason goes to stderr and the step summary. Any other stdout, a
crash, a missing token, an API error, a short page, or a non-pull-request event
runs the real job. A crash says in the step summary that it is not a fast
success. Pushes to main do not call the selector and run the real work.
fast is allowed only when every changed path is outside that job's inputs.
An unclassified path runs the real job. These paths always run all three jobs,
because they are the selector or the workflow that honors it:
.github/workflows/lint.ymlscripts/ci/pr_job_scope.pyscripts/ci/pr_job_scope_step.shscripts/ci/pr_unit_test_selector.py
flowchart TD
start[Lint job] --> kind{pull_request?}
kind -->|no| real[today's checks]
kind -->|yes| api[paginated pulls files API]
api -->|error, short page, or crash| real
api --> paths{every path outside this job?}
paths -->|no| real
paths -->|yes| fast[exit 0 and step summary]
komodo-resources-check¶
The real steps are unchanged when they run: scripts/render-komodo-actions.py --check,
scripts/validate-komodo-resources.py, and the unittest discoveries
test_komodo_*.py, test_trigger_komodo_deploy.py, test_reddit_explorer*.py,
test_orders_*.py, test_gh_pr_dashboard_deployment.py,
test_rexit_deployment.py, and test_oneuptime_stack.py.
Those commands and modules read Komodo resources, Action sources, image policy,
several service trees, and every stack compose file the validator stats. The
scope rule therefore runs the job for any services/** path, including
services/litellm/compose.yml. It also runs for the scripts those tests load
(scripts/trigger-komodo-deploy.py, scripts/komodo-deploy-inputs.py,
scripts/komodo-deploy-inputs.sh, scripts/render-sops-env.py, and the other
Komodo renderers named in scripts/ci/pr_job_scope.py), for
infra/ansible/inventory/** and infra/ansible/ansible.cfg, for the role files
the modules open (the whole github_runner role, three common task/default
files, and the Rexit exporter task plus its three templates), for four
monitoring files (monitoring/prometheus/alerts/komodo.yml,
monitoring/alertmanager/alertmanager.yml,
monitoring/prometheus/alert-schema-baseline.json,
monitoring/grafana/dashboards/alert-triage.json), for
backups/restore-test.sh, backups/restore-drills/rexit.py,
backups/restore-drills/postgresql-companion.sh, and
backups/external/fetch-postgresql-companion.sh, for
.github/workflows/komodo-deploy.yml,
.github/workflows/komodo-image-updates.yml, and
.github/workflows/rexit-release.yml, and for
docs/runbooks/komodo-connect-lxc-server.md. The discovered test modules
themselves are inputs. Other tests/test_rexit_*.py and
tests/test_oneuptime_*.py modules are not.
A docs page other than that runbook, inventory/**, an Ansible playbook, an
unread role file, and monitoring/targets/** do not run this job. An
unclassified script does run it. The tests do not walk all of services/ or
all of .github/workflows/komodo*; those broader trees are not the rule.
services/** still runs because a service path that is not in the proven-fast
set is unclassified.
ansible-lint¶
The job installs collections from infra/ansible/requirements.yml when that
file exists, then runs ansible-lint with project_dir: infra/ansible and
.ansible-lint. Any infra/ansible/** path or a change to .ansible-lint
runs the real lint. A docs-only diff, a services/** diff such as LiteLLM
compose, an inventory/** diff, and a single script outside the scope files
above fast-success. secrets/** and operator/** are not proven irrelevant,
because playbooks look up files there, so those paths run lint.
generators-check¶
The eleven --check commands are the CI rows of the
generator matrix. They read
inventory/hosts/*.yaml, inventory/appliances/*.yaml,
inventory/customer-apps/*.yaml, inventory/services/*.yaml,
inventory/networks.yaml, inventory/oneuptime.yaml,
infra/github-runners/fleet.yml,
infra/github-runners/hyperv-scaler/placements.yml, and
.github/workflows/runner-fleet-dispatch.yml. They compare
infra/ansible/inventory/generated.yml, the Homepage, AdGuard, and OneUptime
generated files, monitoring/targets/nodes.yml,
monitoring/targets/services.yml, docs/architecture/network-diagram.md,
docs/architecture/runner-fleet-diagram.md, and the discovery and Hyper-V
outputs under inventory/ and infra/github-runners/.
Any inventory/** path runs the check, including schema and generator scripts
the job does not execute. A removed or renamed doc stub
(docs/hosts/*.md, docs/appliances/*.md, docs/customer-apps/*.md, or
docs/services/*.md) runs the check because doc-stub --check only stats
those pages. Editing the body of an existing stub does not. services/litellm/compose.yml
is not a generator input. An infra/ansible/** path other than
infra/ansible/inventory/generated.yml is not either.
Diff shapes that skip the real work¶
These are the shapes the rules allow. They are not a measured timing claim.
| Diff | komodo-resources-check | ansible-lint | generators-check |
|---|---|---|---|
| Docs, MkDocs, or markdownlint config, other than the Komodo LXC runbook, generated diagrams, and a deleted doc stub | fast | fast | fast |
services/litellm/compose.yml |
run | fast | fast |
infra/ansible/playbooks/** |
fast | run | fast |
inventory/hosts/*.yaml |
fast | fast | run |
.github/workflows/lint.yml or the scope scripts |
run | run | run |
Still not fast-success¶
These jobs still do their real work on every pull request. This slice does not fast-success them:
python-unit-testskeeps the module selector above. The job itself still runs, and pushes tomainstill discovertests/.phoenix-api-routing,oneuptime-incident-adapter,github-actions-collector, andpaperless-stirling-bridge.- YAML lint, schema validation, SOPS, documentation validation, monitoring checks, patch notification checks, ARA, and both runner-observability jobs.
- Security workflow jobs, including
security-gate. Lint and Security still have nopaths:filter.ci-gateandsecurity-gateare not renamed.