ActionBoxBlog

DevOpsPlatformSRE

Argo CD production sync approval: a Kubernetes GitOps gate

A copy-paste Argo CD production sync approval workflow for Kubernetes GitOps: keep auto-sync safe, review the revision, and fail closed.

ArgoCD's selfHeal: true is a beautiful thing — until it applies a bad manifest to production at 2am. The GitOps model moves the decision to Git, but Git has no opinion about whether right now is the right time. An ArgoCD approval gate inserts a human between "the repo changed" and "the cluster changed".

Argo CD production sync approval: three layers, one decision

LayerWhat it stopsWho decides
PR review + CODEOWNERSBad manifests reaching production branchTeam review
Manual sync (no auto-sync)Any sync without an explicit triggerRelease owner
Approval gateSync before sign-off, with contextA named human, on record

Step 1: disable auto-sync for production

The first gate is structural — production apps never auto-sync:

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments-prod
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://github.com/acme/config.git
    targetRevision: main
    path: overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: payments
  syncPolicy:
    # No 'automated' block — production syncs are always manual
    syncOptions:
      - CreateNamespace=false
      - Validate=true

ArgoCD reports OutOfSync; nothing applies until someone syncs. That's the hook for your gate.

Step 2: ask before the sync

Watch for OutOfSync on production apps and create a decision — with the diff summary attached:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  trigger.on-prod-pending: |
    - when: app.status.sync.status == 'OutOfSync' and app.metadata.labels.environment == 'production'
      send: [actionbox-approval]
  template.actionbox-approval: |
    message: |
      {{.app.metadata.name}} is OutOfSync for production.
      Desired revision: {{.app.status.sync.comparedTo.source.targetRevision}}

The notification template can call your approval endpoint with the app name, namespace, and revision — the approver sees exactly what's waiting.

Step 3: the CI gate with context

For more context than notifications can carry, gate the sync in a pipeline job — the same pattern as any deploy gate:

yaml
name: promote-to-prod
on:
  workflow_dispatch:
    inputs:
      app:
        description: ArgoCD application to sync
        required: true

jobs:
  approval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
       - name: Check ArgoCD status
         run: |
           argocd login $ARGOCD_SERVER --grpc-web --username $ARGOCD_USER --password $ARGOCD_PASS
           argocd app get ${{ inputs.app }} -o json | jq -r '.status.sync.status'

       - name: Install Actionbox CLI
         run: curl -fsSL https://actionbox.cloud/install.sh | sh

       - name: Request sync approval
         id: approval
         env:
           ACTIONBOX_TOKEN: ${{ secrets.ACTIONBOX_TOKEN }}
         run: |
           result=$(actionbox ask "Sync ${{ inputs.app }} to production?" \
             --option approve="Approve sync" \
             --option reject="Reject sync" \
             --context-json "[{\"type\":\"key_value\",\"items\":{\"app\":\"${{ inputs.app }}\",\"environment\":\"production\",\"gate\":\"argocd manual sync + approval\"}}]" \
             --wait --timeout 30m --json)
           echo "decision=$(jq -r '.decision // empty' <<< "$result")" >> "$GITHUB_OUTPUT"

      - name: Sync on approval
        if: steps.approval.outputs.decision == 'approve'
        run: |
          argocd app sync ${{ inputs.app }} --prune
          argocd app wait ${{ inputs.app }} --timeout 300

      - name: Fail on reject or timeout
        if: steps.approval.outputs.decision != 'approve'
        run: |
          echo "Sync not approved: ${{ steps.approval.outputs.decision }}"
          exit 1

Full example: PreSync hook that blocks the sync

A PreSync hook can enforce the approval check inside the cluster — the sync literally cannot proceed without a resolved decision:

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: approval-check
  namespace: argocd
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
        - name: check
          image: alpine/curl:latest
          command:
            - /bin/sh
            - -c
            - |
              STATUS=$(curl -fsS -H "Authorization: Bearer $APPROVAL_TOKEN" \
                "$APPROVAL_API/actions/$APPROVAL_ID/status")
              echo "$STATUS" | grep -q '"decision":"approve"' \
                || { echo "Not approved"; exit 1; }
          env:
            - name: APPROVAL_TOKEN
              valueFrom:
                secretKeyRef: { name: approval-secrets, key: token }
            - name: APPROVAL_ID
              value: "prod-sync-2026-08-16"
            - name: APPROVAL_API
              value: "https://app.actionbox.cloud"
      restartPolicy: Never
  backoffLimit: 0

If the approval doesn't exist or isn't approve, the hook fails, the sync stops, and the app stays OutOfSync — fail closed, with the record intact.

Why this beats "manual sync only"

  • Manual sync controls who can click — approval controls what they approve — context, revision, and risk before the button
  • Decisions on record — who approved which sync to which revision, when, and why
  • Escalation built in — a pending sync has an SLA and a queue, not a Slack thread
  • Same gate for every environment — staging can require one approver, production two, with identical plumbing

Try it

  1. Install the CLI on your release host or runner
  2. Create a Source → ACTIONBOX_TOKEN
  3. Add the approval step before argocd app sync in your promote job

Create a free Source · GitHub Actions + GitOps reference · Deploy approval gates in GitHub Actions

Try this workflow in minutes

Create a free Source, then run the exact commands from this post against the live API — no approval infrastructure to build.

S
Suson Sapkota

Founder, Actionbox