id: argocd-single-tenant-wave-rollout
namespace: company.team
description: |
Roll out Argo CD Application updates across a large single-tenant fleet using
controlled waves and Status-based gates.
inputs:
- id: tenant_id
type: STRING
defaults: tenant-a
description: Tenant identifier used for logs and rollout traceability.
- id: application_waves
type: JSON
description: |
Ordered list of deployment waves. Each wave is a list of Argo CD
Application names.
defaults: |
[
["tenant-a-account", "tenant-a-billing", "tenant-a-auth"],
["tenant-a-orders", "tenant-a-payments", "tenant-a-inventory"],
["tenant-a-api-gateway", "tenant-a-web"]
]
- id: max_parallel_per_wave
type: INT
defaults: 5
description: Maximum number of applications deployed in parallel per wave.
- id: target_revision
type: STRING
defaults: HEAD
description: Git revision used by manual sync operations.
- id: prune
type: BOOL
defaults: true
description: Whether manual sync should prune out-of-band resources.
- id: sync_when_auto_sync_enabled
type: BOOL
defaults: false
description: |
If true, run a manual sync even when Argo CD auto-sync is enabled.
If false, rely on Argo CD reconciliation and only gate on Status.
- id: auto_sync_disabled_strategy
type: STRING
defaults: manual_approval
description: |
Strategy when auto-sync is disabled:
- force_sync: sync immediately
- manual_approval: pause execution, then sync after resume
- fail_rollout: fail immediately
- skip_application: don't sync that app, continue with next one
- id: manual_approval_timeout
type: STRING
defaults: PT2H
description: Maximum pause duration when manual approval is required.
- id: readiness_poll_interval
type: STRING
defaults: PT20S
description: Delay between readiness checks.
- id: readiness_max_attempts
type: INT
defaults: 90
description: Max readiness check attempts before failing the application.
- id: allow_degraded_health
type: BOOL
defaults: false
description: |
If true, `Degraded` can be accepted as healthy for readiness.
Keep false for strict production gates.
tasks:
- id: rollout_start
type: io.kestra.plugin.core.log.Log
message: |
Starting Argo CD rollout for tenant={{ inputs.tenant_id }}.
Waves={{ inputs.application_waves }},
max_parallel_per_wave={{ inputs.max_parallel_per_wave }},
auto_sync_disabled_strategy={{ inputs.auto_sync_disabled_strategy }},
sync_when_auto_sync_enabled={{ inputs.sync_when_auto_sync_enabled }}.
# Waves are processed sequentially to enforce blast-radius control.
- id: rollout_by_wave
type: io.kestra.plugin.core.flow.ForEach
concurrencyLimit: 1
values: "{{ inputs.application_waves }}"
tasks:
- id: wave_start
type: io.kestra.plugin.core.log.Log
message: "Starting deployment wave: {{ taskrun.value }}"
# Applications in the same wave are deployed in parallel with a cap.
- id: rollout_applications_in_wave
type: io.kestra.plugin.core.flow.ForEach
concurrencyLimit: 5
values: "{{ parent.taskrun.value }}"
tasks:
- id: status_before
type: io.kestra.plugin.argocd.apps.Status
description: Fetch baseline Argo CD status before any action.
application: "{{ taskrun.value }}"
- id: log_status_before
type: io.kestra.plugin.core.log.Log
message: |
[PRE] app={{ taskrun.value }}
sync={{ outputs.status_before.app.status.sync.status }}
health={{ outputs.status_before.app.status.health.status }}
operation={{ outputs.status_before.app.status.operationState.phase | default('N/A') }}
auto_sync_enabled={{ outputs.status_before.app.spec.syncPolicy.automated != null }}
# Branch explicitly when auto-sync is disabled on the application.
- id: handle_auto_sync_disabled
type: io.kestra.plugin.core.flow.If
condition: "{{ outputs.status_before.app.spec.syncPolicy.automated == null }}"
then:
- id: auto_sync_disabled_strategy_switch
type: io.kestra.plugin.core.flow.Switch
value: "{{ inputs.auto_sync_disabled_strategy }}"
cases:
force_sync:
- id: log_force_sync
type: io.kestra.plugin.core.log.Log
message: "[ACTION] auto-sync disabled for {{ taskrun.value }} -> force_sync"
- id: sync_force_sync
type: io.kestra.plugin.argocd.apps.Sync
application: "{{ taskrun.value }}"
revision: "{{ inputs.target_revision }}"
prune: "{{ inputs.prune }}"
manual_approval:
- id: log_manual_approval
type: io.kestra.plugin.core.log.Log
message: |
[ACTION] auto-sync disabled for {{ taskrun.value }} -> manual_approval.
Resume execution to approve sync for this app.
- id: wait_for_manual_approval
type: io.kestra.plugin.core.flow.Pause
pauseDuration: "{{ inputs.manual_approval_timeout }}"
- id: sync_after_manual_approval
type: io.kestra.plugin.argocd.apps.Sync
application: "{{ taskrun.value }}"
revision: "{{ inputs.target_revision }}"
prune: "{{ inputs.prune }}"
fail_rollout:
- id: fail_on_auto_sync_disabled
type: io.kestra.plugin.core.execution.Fail
errorMessage: |
auto-sync is disabled for {{ taskrun.value }} and strategy=fail_rollout.
Stopping rollout to avoid unsafe reconciliation.
defaults:
- id: skip_application
type: io.kestra.plugin.core.log.Log
message: |
[ACTION] auto-sync disabled for {{ taskrun.value }} and strategy
'{{ inputs.auto_sync_disabled_strategy }}' -> skip_application.
else:
- id: handle_auto_sync_enabled
type: io.kestra.plugin.core.flow.If
condition: "{{ inputs.sync_when_auto_sync_enabled }}"
then:
- id: log_sync_when_enabled
type: io.kestra.plugin.core.log.Log
message: "[ACTION] auto-sync enabled for {{ taskrun.value }} -> manual sync
requested"
- id: sync_when_enabled
type: io.kestra.plugin.argocd.apps.Sync
application: "{{ taskrun.value }}"
revision: "{{ inputs.target_revision }}"
prune: "{{ inputs.prune }}"
else:
- id: log_no_manual_sync_when_enabled
type: io.kestra.plugin.core.log.Log
message: |
[ACTION] auto-sync enabled for {{ taskrun.value }} -> no manual sync.
Waiting for desired state through Status checks.
# Poll Argo CD Status and fail-fast if the app does not converge.
- id: wait_until_synced_and_healthy
type: io.kestra.plugin.core.flow.Sequential
runIf: "{{ not (outputs.status_before.app.spec.syncPolicy.automated == null and
inputs.auto_sync_disabled_strategy == 'skip_application') }}"
retry:
type: constant
interval: PT20S
maxAttempt: 90
warningOnRetry: false
tasks:
- id: readiness_status
type: io.kestra.plugin.argocd.apps.Status
application: "{{ taskrun.value }}"
- id: log_readiness_probe
type: io.kestra.plugin.core.log.Log
message: |
[PROBE] app={{ taskrun.value }}
sync={{ outputs.readiness_status.app.status.sync.status }}
health={{ outputs.readiness_status.app.status.health.status }}
operation={{ outputs.readiness_status.app.status.operationState.phase ?? 'N/A' }}
sync_message={{ outputs.readiness_status.app.status.sync.comparedTo.source.targetRevision ?? 'N/A' }}
- id: assert_app_ready
type: io.kestra.plugin.core.execution.Assert
conditions:
- "{{ outputs.readiness_status.app.status.sync.status ==
'Synced' }}"
- "{{ outputs.readiness_status.app.status.health.status ==
'Healthy' or (inputs.allow_degraded_health and
outputs.readiness_status.app.status.health.status ==
'Degraded') }}"
- id: status_after
type: io.kestra.plugin.argocd.apps.Status
description: Capture final status after rollout logic for this app.
application: "{{ taskrun.value }}"
- id: log_status_after
type: io.kestra.plugin.core.log.Log
message: |
[POST] app={{ taskrun.value }}
sync={{ outputs.status_after.app.status.sync.status }}
health={{ outputs.status_after.app.status.health.status }}
operation={{ outputs.status_after.app.status.operationState.phase ?? 'N/A' }}
revision={{ outputs.status_after.app.status.sync.revision ?? 'N/A' }}
# Emit a concise machine-readable summary for downstream reporting.
- id: emit_app_summary
type: io.kestra.plugin.core.debug.Return
format: |
{
"tenant": "{{ inputs.tenant_id }}",
"application": "{{ taskrun.value }}",
"sync": "{{ outputs.status_after.app.status.sync.status }}",
"health": "{{ outputs.status_after.app.status.health.status }}",
"operation": "{{ outputs.status_after.app.status.operationState.phase ?? 'N/A' }}",
"autoSyncEnabled": {{ outputs.status_after.app.spec.syncPolicy.automated != null }}
}
- id: wave_done
type: io.kestra.plugin.core.log.Log
message: "Completed deployment wave: {{ taskrun.value }}"
- id: rollout_done
type: io.kestra.plugin.core.log.Log
message: "Tenant {{ inputs.tenant_id }} rollout completed for all waves."
errors:
- id: rollout_failure_diagnostics
type: io.kestra.plugin.core.log.Log
message: |
Rollout failed for tenant={{ inputs.tenant_id }}.
execution={{ execution.id }}.
last_error={{ errorLogs()[0]['message'] }}.
Review the latest [PRE]/[PROBE]/[POST] logs for the failing application.
pluginDefaults:
- type: io.kestra.plugin.argocd.apps.Status
values:
server: "{{ secret('ARGOCD_SERVER') }}"
token: "{{ secret('ARGOCD_TOKEN') }}"
retry:
type: constant
interval: PT10S
maxAttempt: 3
warningOnRetry: false
- type: io.kestra.plugin.argocd.apps.Sync
values:
server: "{{ secret('ARGOCD_SERVER') }}"
token: "{{ secret('ARGOCD_TOKEN') }}"
retry:
type: constant
interval: PT10S
maxAttempt: 3
warningOnRetry: false