Change detection
Change detection decides what a commit must re-verify, and it does so from the same
declared graph as everything else: component ownership from component.yaml, edges from
dependsOn, build inputs from change.inputs, and intent sensitivity from
change.watches. When it is unsure, it over-reports, so a change cannot quietly skip the
standards its components are held to.
orun can narrow inspection, planning, and execution to changed files or changed components. That is useful in pull requests, preview environments, and incremental review workflows.
Change detection is computed by a single engine over the object-model catalog — the same engine behind orun catalog affected and the cockpit's changed/affected overlay. It classifies a change using the catalog's ownership map and dependency graph, and over-reports rather than under-reports on ambiguity, so a missed change never silently drops out of a plan. To inspect the engine's classification directly (the directly-changed, dependents, affected, and selection sets, with a confidence signal), use orun catalog affected --json.
The catalog always matches the working tree
--changed reads the persisted catalog snapshot at catalogs/current to learn
the components, their paths, and their dependsOn edges. Before it selects
anything, plan, run, and component --changed check that snapshot against
the working tree and rebuild it when it is stale, the same way
orun catalog refresh does. So a component.yaml (or component.yml) that
was added, moved, or edited since the last refresh counts, including after a
branch switch, in a fresh git worktree, in a gitignored directory, or in a
workspace without git.
The check is cheap. The snapshot is keyed on the git source and on a digest of
intent.yaml plus every component manifest discovery finds under the intent's
discovery.roots (the whole workspace when none are declared; see
where the catalog looks). When
nothing changed, the existing snapshot is reused without resolving. When the
rebuilt snapshot has a different component graph from the old one, one line on
stderr says so:
orun: catalog was stale (1 component added); refreshed from the working tree
orun plan --changed --no-catalog-refresh skips the rebuild and uses the
snapshot as it is. If that snapshot was not built from the current working
tree, orun prints a warning that names orun catalog refresh, and it stops
with an error if there is no snapshot yet.
When the working tree cannot be resolved into a catalog, --changed fails with
the resolver's error rather than selecting from a stale or missing snapshot.
The most common cause is two manifests that declare the same metadata.name;
the error names every file involved:
✕ failed to compute changed components: resolve catalog from the working tree: catalogresolve: duplicate component name "api-edge" (componentKey default/cirrus/api-edge) declared in both apps/api-edge/component.yaml and variations/blue/overlay/apps/api-edge/component.yaml; every component manifest discovery finds must have a unique metadata.name (exclude copies with catalog.discovery.exclude, or narrow discovery.roots)
Commands that support change detection
orun planorun componentorun run
Use those commands when you want to focus on the parts of the platform graph that were touched by a branch, commit range, or explicit file list.
Available flags
| Flag | Purpose |
|---|---|
--changed | Enable change-aware filtering based on git state |
--base <ref> | Set the base ref used for comparison |
--head <ref> | Set the head ref used for comparison |
--files <path,...> | Override git diff resolution with an explicit file list |
--uncommitted | Scope detection to uncommitted changes |
--untracked | Scope detection to untracked files |
--intent-impact <mode> | How global intent changes affect components (watch/all/none, default: watch) |
--explain | Print how --changed resolved its base and head refs (useful for debugging) |
CI auto-detection
When --changed is used inside a CI environment without explicit --base or --head flags, orun automatically infers the correct refs from environment variables:
| CI system | Base ref source | Head ref source |
|---|---|---|
GitHub Actions (pull_request) | GITHUB_BASE_REF | GITHUB_SHA |
GitHub Actions (push) | Previous commit | GITHUB_SHA |
| Other CI | Falls back to main | HEAD |
This means a simple --changed flag is sufficient in most CI pipelines — no manual ref wiring needed:
orun plan --changed
orun run --changed
Use --explain to see exactly which refs were resolved:
orun plan --changed --explain
Pull request review flow
orun component \
--intent examples/intent.yaml \
--changed \
--base main \
--long
orun plan \
--intent examples/intent.yaml \
--changed \
--base main \
--output /tmp/pr-plan.json
To generate and run a change-scoped plan in a single step:
orun run --changed --base main
Explicit file lists
When your CI system already knows the changed files, pass them directly instead of asking orun to compute the diff:
orun plan \
--intent examples/intent.yaml \
--files examples/infra/infra-1/component.yaml,examples/intent.yaml \
--output /tmp/filtered-plan.json
Local development flow
For uncommitted work in a repository checkout:
orun component \
--intent examples/intent.yaml \
--changed \
--uncommitted
Debugging ref resolution
When --changed produces unexpected results, --explain shows the full resolution trace — which CI variables were detected, which git commands were run, and which refs were ultimately used:
orun plan --changed --explain
orun run --changed --explain
Use change detection to reduce noise during review, not to hide full-environment validation when you need a canonical plan for release.
How a changed file selects components
A changed file selects the component that owns it: the component whose
directory (or spec.path) contains the file, deepest path wins. Selection then
propagates to dependents over dependsOn input: true edges,
transitively.
A component owns only its own directory, so a file at the repository root —
pnpm-lock.yaml, turbo.json, tooling/eslint/** — selects nothing unless a
component declares it in change.inputs.
Input globs (spec.change.inputs)
spec.change.inputs lists glob patterns for files a component depends on but
does not own. It sits next to spec.change.watches:
watches react to intent sections, inputs react to repository files. A changed
file matching any input glob selects the component, in addition to normal
path ownership, and selection then propagates over input edges exactly as for
an owned file.
apiVersion: sourceplane.io/v1
kind: Component
metadata:
name: web
spec:
type: turbo-app
change:
inputs:
- pnpm-lock.yaml # the workspace lockfile
- turbo.json # the task pipeline
- tooling/eslint/** # shared lint config, any depth
changed: pnpm-lock.yaml -> plan: web
changed: tooling/eslint/rules/x.js -> plan: web (+ its input:true dependents)
changed: tooling/prettier/index.js -> plan: (nothing — no glob matches)
Rules:
- Patterns are relative to the repository root (not the component
directory) and use doublestar syntax:
*and?match within one path segment,**matches any number of segments, plus[...]classes and{a,b}alternation. - Absolute paths,
.or..segments, backslashes, empty segments, and malformed globs are rejected when the catalog is resolved (component.spec.change.inputs.invalid) and when the intent is normalized, so a typo fails closed instead of silently selecting nothing. - Inline
components:entries inintent.yamlaccept the samechange.inputs:list. - A component without
change.inputsbehaves exactly as before.
orun catalog affected --explain records which pattern matched:
explain:
- ns/repo/web: change.inputs glob pnpm-lock.yaml matched: pnpm-lock.yaml
Intent-aware change scoping
When intent.yaml itself is in the changed file set, orun performs a semantic diff to determine whether the change affects all components or only specific inline components:
| What changed in intent.yaml | Effect |
|---|---|
Top-level env, environments, groups, discovery, compositions, automation, or execution | Only components with matching change.watches are marked changed |
Only entries under top-level components: [] | Only the added/modified/removed inline component names are marked changed |
| Formatting, comments, or component reordering without content change | No components marked changed from intent |
Both components and another section | Global sections use watch matching; inline component changes are always direct |
By default, a global intent section change does not mark any component as changed
unless that component opts in with spec.change.watches:
apiVersion: sourceplane.io/v1
kind: Component
metadata:
name: api-platform
spec:
type: terraform
domain: platform
subscribe:
environments: [dev, staging, production]
change:
watches:
- environments
- groups
| Watch value | Marks the component changed when this part of intent.yaml changes |
|---|---|
environments | environments: |
groups | groups: |
env | Root-level env: |
automation | automation: |
compositions | compositions: |
discovery | discovery: |
execution | execution: |
With no watches (or an empty list), only direct file changes select the component: files
under its path, its component.yaml, and any change.inputs match.
--intent-impact overrides the watch behaviour for one command:
orun plan --changed --intent-impact=watch # default: only matching watches
orun plan --changed --intent-impact=all # any global intent change marks every component
orun plan --changed --intent-impact=none # ignore intent changes; files only
--intent-impact=all restores the behaviour before v2.3.0, when any global intent change
marked every component. Prefer adding watches to the components that really depend on a
section.
If the base or head intent cannot be parsed (e.g. the file is new), the engine escalates to a global intent change (over-reporting) rather than risk missing an affected component.
To inspect which components a change selected and why, query the engine directly:
orun catalog affected --json
# {
# "directlyChanged": ["api-platform"],
# "dependents": ["web-gateway"],
# "affected": ["api-platform", "web-gateway"],
# "selection": ["api-platform"],
# "intentMode": "components",
# "confidence": "high",
# "needsFullResolve": false
# }
--explain on plan/run covers ref resolution (which base/head were used); the per-component classification and its provenance live in orun catalog affected.
Selection and blast radius
The engine computes two different sets, and it matters which one you are looking at:
- Selection is what
--changedplans and runs: the directly changed components, plus components that declare one of them as a build input (dependsOnwithinput: true, transitively), plus dependencies pulled in withinclude: always. - Affected is the blast radius: the directly changed components plus everything that
depends on them. It drives the cockpit's affected overlay and
orun catalog affected. (orun task checkcompares a task contract'saffectswith the directly changed components: the contract limits what the work edits, not what rebuilds.)
So if web-gateway depends on a changed api-platform through an ordinary dependsOn
edge, web-gateway is in the blast radius but is not planned. Mark the edge
input: true when the dependent's build embeds the dependency (a shared package, a
generated client). include: always works in the other direction: it pulls a dependency
into the plan whenever its dependent is selected (see
dependency rules).
The full pipeline:
changed files
→ file-to-component ownership (paths + change.inputs globs)
→ intent semantic diff (changed sections) → components with matching change.watches
→ build-input dependents (dependsOn input:true, transitive) = directly changed
→ + include:always dependencies = selection (planned)
→ + reverse dependents = affected (blast radius)
Trigger-driven change detection
When using trigger bindings with plan.scope: changed, the trigger system automatically enables change detection with the base/head refs resolved from the event payload:
automation:
triggerBindings:
github-pull-request:
on:
provider: github
event: pull_request
baseBranches: [main]
plan:
scope: changed
base: pull_request.base.sha
head: pull_request.head.sha
When this trigger fires via --from-ci github --event-file "$GITHUB_EVENT_PATH", orun resolves base and head from the event JSON and enables --changed mode automatically. No explicit --changed flag is needed:
orun plan --from-ci github --event-file "$GITHUB_EVENT_PATH"
CLI --base and --head flags override trigger-derived values when you need manual control.