Skip to main content

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 plan
  • orun component
  • orun 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​

FlagPurpose
--changedEnable 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
--uncommittedScope detection to uncommitted changes
--untrackedScope detection to untracked files
--intent-impact <mode>How global intent changes affect components (watch/all/none, default: watch)
--explainPrint 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 systemBase ref sourceHead ref source
GitHub Actions (pull_request)GITHUB_BASE_REFGITHUB_SHA
GitHub Actions (push)Previous commitGITHUB_SHA
Other CIFalls back to mainHEAD

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 in intent.yaml accept the same change.inputs: list.
  • A component without change.inputs behaves 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.yamlEffect
Top-level env, environments, groups, discovery, compositions, automation, or executionOnly 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 changeNo components marked changed from intent
Both components and another sectionGlobal 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 valueMarks the component changed when this part of intent.yaml changes
environmentsenvironments:
groupsgroups:
envRoot-level env:
automationautomation:
compositionscompositions:
discoverydiscovery:
executionexecution:

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 --changed plans and runs: the directly changed components, plus components that declare one of them as a build input (dependsOn with input: true, transitively), plus dependencies pulled in with include: 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 check compares a task contract's affects with 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.