| Category | Status | Created | Author |
|---|---|---|---|
| Policies | Draft | 2026-03-13 | Justin Brooks |
Summary
Add aPlannable interface to job agents that lets ctrlplane compute the
rendered deployment output for a release target without dispatching a job. By
comparing the rendered output hash of a proposed version against the hash of the
currently deployed release, ctrlplane can mechanically determine which release
targets are actually affected by a version change. Unaffected targets can then
be fast-tracked through the promotion lifecycle.
Motivation
RFC 0001 (Scoped Versions) introduces a way for deployers to declare which targets a version affects. This works well when the deployer knows the impact upfront — a regional hotfix, a single-service config change. But it relies on the deployer providing accurate scope. If the scope is wrong, targets are either unnecessarily delayed (too broad) or silently skipped (too narrow). The external systems ctrlplane dispatches to — ArgoCD, Terraform Cloud, Helm, Kubernetes — already know how to compute what a deployment would produce without actually applying it. ArgoCD renders Application manifests from templates. Terraform produces execution plans. Helm hashelm template. These
systems can answer the question “would this version change anything for this
target?” with mechanical precision.
Today, ctrlplane cannot leverage this knowledge. The rendering happens inside
the job agent at dispatch time, and the result is never captured or compared.
ctrlplane treats every new version as a change for every target because it
operates on version identity (version ID differs → release differs), not on
rendered output identity (rendered manifest is the same → nothing changed).
Why version identity is insufficient
A release’s content hash (Release.ContentHash()) includes the version ID and
tag:
Why rendering is the right level to compare
The rendered output is what actually gets applied to the target system. Crucially, this is not the intermediate representation ctrlplane produces (like an ArgoCD Application CRD or a Terraform variable file) — it is the final output that the external system produces after it processes that intermediate input. For ArgoCD, this means the Kubernetes manifests after fetching the git repo and rendering the Helm chart. For Terraform, this means the execution plan after evaluating all modules and state. ctrlplane’s in-process template rendering (e.g.,TemplateApplication) produces
the input to the external system, not the deployed output. A version change
almost always changes this input (the targetRevision, the image tag in Helm
values, etc.). But the external system may still produce identical output — for
example, when a git commit only modifies files for a different service than the
one this Helm chart deploys.
The only way to know whether the deployed state would actually change is to ask
the external system to render the final output. This is what terraform plan,
argocd app diff, and helm template do. Plan-based diff detection brings this
capability into ctrlplane’s promotion lifecycle by delegating the rendering to
the system that owns it.
Relationship to RFC 0001
Scoped versions (RFC 0001) and plan-based diff detection are complementary:- Scoped versions are fast and explicit — the deployer states intent, the reconciler filters instantly, no external calls needed.
- Plan-based diffs are accurate and automatic — the external system computes impact, no deployer knowledge required, but adds latency from the plan call.
Proposal
New interface: Plannable
Add an optional interface to the job agent type system alongside the existing
Dispatchable and Verifiable:
Verifiable:
Registry extension
The job agent registry already checks for optional interfaces. Add aPlan
method following the same pattern as AgentVerifications:
Plannable, the registry returns nil and the
reconciler falls back to treating the version as a change for all targets
(current behavior).
Schema
Store the rendered content hash on the release target state so it can be compared against future plan results:Reconciler integration
The plan step fits into the desired release reconciler as an optional phase between candidate selection and policy evaluation:- Skip the version for this target (move to the next candidate)
- Fast-track the version through policy evaluation (auto-satisfy gates)
Policy integration
Plan results feed into the policy pipeline through a new optional policy rule type:diffCheck. This rule evaluates the plan result and can auto-satisfy
other gates when no diff is detected:
HasChanges = false, the rules
listed in skip_when_no_diff are automatically satisfied. The version still
advances through the pipeline (the release is created, the release target state
updates), but blocking gates are bypassed.
If no diffCheck policy is configured, plan results are informational only —
stored for audit and displayed in the UI but not used to alter promotion flow.
The diffCheck evaluator would be added to the evaluator set in policyeval.go
alongside the existing evaluators:
Agent implementations
ArgoCD
The ArgoCD agent’s in-processTemplateApplication function renders a Go
template into an ArgoCD Application CRD. This is not the right level to
diff. The Application spec contains fields like targetRevision and
helm.values that reference version.tag — so the rendered Application CRD
will always differ between versions, even when the final deployed manifests are
identical.
The actual deployed state is what ArgoCD produces from the Application spec:
it fetches the git repo at the specified revision, renders the Helm chart (or
kustomize overlay, or plain manifests), and produces the final Kubernetes
manifests that get applied to the cluster. Two different git revisions can
produce identical rendered manifests if the files that changed in the commit are
irrelevant to the chart or overlay being used.
To compute a real diff, the Plan implementation must call the ArgoCD API to
get the fully rendered manifests. The ArgoCD Go client already used by the agent
(ApplicationServiceClient) exposes GetManifests for exactly this:
version.tag changes. But
if the Helm chart at the new tag only changed a values file for a different
service, the rendered Kubernetes manifests for this resource may be identical.
Only ArgoCD — which actually fetches and renders the chart — can tell you that.
Terraform Cloud
Terraform Cloud has native plan support. ThePlan implementation would trigger
a speculative plan run via the API and return the plan’s resource change
summary:
GitHub Actions
GitHub Actions does not have a native plan/dry-run concept. The agent would not implementPlannable, and the registry returns nil. Targets using GitHub
Actions fall back to current behavior — every version is treated as a change.
Storing the deployed hash
When a job completes successfully, the reconciler updates the release target’srendered_content_hash:
HasChanges defaults to true.
UI
- Release target view — When a plan result exists, show a “No changes detected” or “Changes detected” indicator alongside the version evaluation. For targets with no changes, display a muted state to signal the version is advancing without operational impact.
- Diff viewer — When
Diffis populated, provide an expandable panel showing the human-readable diff (YAML diff for ArgoCD, resource summary for Terraform). - Version detail — Aggregate plan results across all release targets to show “X of Y targets affected” on the version page.
Async plan execution
AllPlannable agents involve network calls — ArgoCD must fetch the git repo
and render charts, Terraform Cloud must run a speculative plan. Plans should run
asynchronously:
- The reconciler enqueues a plan request when it encounters a new candidate version for a release target.
- A plan worker processes the request, calls the agent’s
Planmethod, and stores the result inrelease_target_plan. - On the next reconciliation pass, the stored plan result is available and the reconciler uses it to determine diff status.
Examples
ArgoCD: Helm chart change affecting one service
A deployment manages 20 clusters across 4 environments. A new version points to a new git commit that updates the Helm chart’svalues.yaml for the payment
service. The ArgoCD Application template sets targetRevision to the version
tag.
- Version
v3.1.0is created. The git commit behind this tag only modifiescharts/payment/values.yaml. - The reconciler enqueues a plan for each release target.
- For each target, the plan worker renders the ArgoCD Application CRD (which
differs for every target because
targetRevisionchanged), then calls ArgoCD’sGetManifestsAPI to get the fully rendered Kubernetes manifests at that revision. - For the 4 clusters that deploy the payment chart, ArgoCD’s rendered manifests
differ from the stored hash —
HasChanges = true. - For the 16 clusters that deploy other charts from the same repo, ArgoCD
renders the same manifests as the previous version (the files that changed
are irrelevant to their charts) —
HasChanges = false. - The
diffCheckpolicy auto-satisfies environment progression and approval for the 16 unaffected clusters. - The 4 affected clusters go through the full promotion lifecycle.
Terraform Cloud: Infrastructure change scoped to one region
A Terraform deployment manages infrastructure in 3 regions. A version changes an IAM policy that only applies to us-east-1.- Version
v1.5.0is created. - The reconciler triggers speculative plans for each region’s release target.
- The us-east-1 plan shows 1 resource change. The other two plans show 0 changes.
- Only the us-east-1 target enters the full promotion pipeline.
GitHub Actions: No plan support (fallback)
A deployment uses GitHub Actions as its job agent. GitHub Actions does not implementPlannable.
- Version
v2.0.0is created. - The reconciler calls
registry.Plan()— returns nil. - All release targets enter the promotion pipeline as usual.
- No change from current behavior.
Migration
- The
rendered_content_hashcolumn is additive and nullable. Existing release targets start withNULL, meaning the first plan comparison always treats the target as changed (fail-open). - The
release_target_plantable is new and requires no data migration. - Agents that do not implement
Plannablecontinue to work without changes. - The
diffCheckpolicy rule is optional. Without it, plan results are informational only.
Open Questions
- Should plan results block or only fast-track? The current proposal only uses plan results to skip policy gates (fast-track). An alternative is to block versions that show no changes from creating releases at all, similar to how scoped versions filter candidates. The risk is that a plan bug could prevent legitimate deployments.
- Cost of Plans. Each plan consumes resources. For deployments with many release targets, the plan step could generate significant API load. Should planning be opt-in per deployment, or rate-limited? For deployments with many release targets, the plan step could generate significant API load. Should planning be opt-in per deployment, or rate-limited?
-
Interaction with RFC 0001. If a version has a
targetSelector(RFC 0001) that excludes a target, should the plan still run for that target? The proposed order (filter by target selector, then plan) means excluded targets are never planned, which is efficient but means you cannot use plan results to validate a target selector’s correctness.