| Category | Status | Created | Author |
|---|---|---|---|
| Policies | Draft | 2026-03-13 | Justin Brooks |
Summary
Add an optionaltargetSelector field to deployment versions that limits which
release targets a version flows through the promotion lifecycle for. This allows
deployers to express “this version only affects these targets” at creation time,
so unaffected targets skip the full policy pipeline entirely.
Motivation
Ctrlplane already handles two kinds of changes differently:- Variable changes roll out instantly. When a deployment variable or resource variable is updated, the affected release targets are re-reconciled with the new variable values. The version hasn’t changed, so policies that already passed (approval, environment progression, verification) remain satisfied. The new release is created with updated variables and a job is dispatched immediately.
-
Version changes go through the full promotion lifecycle. When a new
deployment version is created with
status: ready, ctrlplane creates releases for every release target in the deployment’s matrix (Deployment x Environment x Resource). Each release goes through environment progression, approval gates, verification, gradual rollout, and cooldown before a job is created.
Why ctrlplane cannot derive version impact automatically
For variable changes, ctrlplane can detect impact mechanically: it resolves the new variable values, compares them to the current release’s variables, and only creates new releases where the resolved values actually differ. This is why variable changes can roll out instantly — ctrlplane knows exactly what changed. Version changes are fundamentally different. A release is defined asVersion + Environment + Resource + Resolved Variables. When a new version is
created, the version component is always new — that is the entire reason the
release exists. Even if every resolved variable is identical across targets, the
version ID differs, so every release is “different” from ctrlplane’s
perspective. You cannot diff away the version itself.
The knowledge of which targets are truly impacted by a version change comes from
the deployer’s understanding of what the change means — which config files
changed in the Helm chart, which services are affected by the new image, which
regions need the update. This is semantic knowledge about the change that exists
outside ctrlplane’s data model. Ctrlplane sees a new version and treats it as a
new version for all targets; it cannot know that “this Helm chart change only
affects the payment service” or “this image bump doesn’t change behavior for
clusters running the old schema.”
Scoped versions acknowledge this reality by giving the deployer a structured way
to express their knowledge, rather than trying to derive it mechanically. The
same way ctrlplane already trusts that variable selectors correctly express
which targets a variable value applies to, scoped versions let the deployer
express which targets a version applies to.
Comparison with existing mechanisms
Version Selectors are policy rules that answer “is this version allowed to deploy to this target?” They are eligibility gates — a version that fails a selector shows as blocked/denied in the UI and in rule evaluations. This is semantically wrong for the scoped version use case: the version is not bad for unaffected targets, it is simply irrelevant. Version selectors also don’t exempt matching targets from other policy rules — a version that passes the selector still goes through the full promotion chain. Policy Skips allow bypassing individual policy rules for a version + environment. They work today, but require the deployer to know specific rule IDs, create skips per-rule per-environment, and the version still appears in the evaluation pipeline for every target. They are an escape hatch, not a first-class workflow. Scoped Versions operate before the policy pipeline. The reconciler skips the version entirely for non-matching targets — no releases created, no policy evaluations run, no “denied” entries in the UI. The intent (“this version is for these targets”) lives on the version itself, making it auditable and declarative.Proposal
Schema
Add an optionaltarget_selector column to the deployment_version table:
NULL, the version targets all release targets (current behavior). When
set, it contains a CEL expression evaluated against the release target’s
resource, environment, and deployment.
API
Extend the version creation endpoints to accept the new field. REST API:resource, environment, and deployment.
Reconciler changes
In the desired release reconciler, thefindDeployableVersion function iterates
candidate versions newest-first and evaluates policy rules. The target selector
check should be inserted before policy evaluation, as a pre-filter on the
candidate version list:
reconcile.go, after GetCandidateVersions returns, filter the
list:
targetSelector that does not match the current release target
are silently removed from the candidate list. The reconciler then proceeds as
normal with the remaining candidates. If no candidates remain, the release
target keeps its current state.
UI
The web UI should surface scoped versions in a few places:- Version list — Show a badge or indicator when a version has a
targetSelector, with the expression visible on hover. - Release target view — When a version is scoped and doesn’t match a target, it should not appear in that target’s version evaluation list at all (as opposed to appearing as “denied”).
- Version creation — Optionally expose the
targetSelectorfield in the UI when creating versions manually.
Behavior with other policy rules
Scoped versions interact cleanly with existing policy rules:- Environment progression: Only evaluated for targets that match the scope. If a scoped version targets production directly and no staging targets match, the environment progression rule is only evaluated for production targets. The deployer is responsible for ensuring this makes sense — the scope is an explicit declaration of intent.
- Approval: Approvals are per-environment. Only environments with matching targets will require approval.
- Gradual rollout: Rollout only applies across matching targets, naturally reducing the rollout surface.
- Version cooldown: Evaluated per-target as before, but only for targets in scope.
Fallback behavior
IftargetSelector evaluation fails (malformed CEL, missing fields), the
version should be included in the candidate list (fail-open). This prevents
a typo in a selector from silently dropping a version for all targets. The
failure should be logged as a warning.
Examples
Hotfix for a single region
Config change for a specific environment
Broad rollout (default behavior)
targetSelector — all release targets are considered. Identical to current
behavior.
Migration
- The schema change is additive (
ADD COLUMN ... NULL), requiring no data migration. - Existing versions have
target_selector = NULL, preserving current behavior. - No changes to existing policies or release targets are needed.
- The reconciler change is backwards-compatible: versions without a selector pass through the filter unchanged.
Open Questions
- Should scoped versions interact with environment progression? If a version scopes to production only, should environment progression rules block it (staging hasn’t seen it) or should the scope be treated as an explicit override of progression? The current proposal lets the deployer handle this — they can combine the scope with policy skips if needed.
- Should there be a permission or policy guard on scoping? Scoped versions let deployers bypass the normal promotion surface area. Organizations may want to restrict who can create scoped versions, or require that scoped versions still pass through certain gates.
- Should the selector support resource-only, or also environment and deployment fields? The proposal includes all three for flexibility, but simpler scoping (resource-only) might be sufficient and easier to reason about.
-
Naming:
targetSelectorvsscopevsaffectedTargets— what conveys the intent most clearly?