GitOps Deployments with Argo CD and GitHub Actions

An Argo CD release splits in two: the GitHub Actions workflow builds by digest and commits the manifest, and the in-cluster controller applies it.

Last verified:

An Argo CD release splits a deployment across two systems: the GitHub Actions workflow builds the image, resolves its digest, and commits that digest into a manifest in a config repository, and the Argo CD controller running inside the cluster picks the commit up and applies it. The workflow therefore holds a registry credential and a git write token, while a kubeconfig never enters the run, which is what takes cluster credentials out of GitHub Actions entirely.

This page gives the responsibility split job by job, a workflow file that builds by digest and commits the manifest change with no cluster credentials present, the verification job that waits for the rollout and reports its result back to the run, runner sizes and per-minute rates for each of the three jobs, and the failure modes that appear once every merge goes through this path.

Overview

Argo CD follows the GitOps model, where the desired configuration is pushed to git first and the cluster state then syncs to what git says (automation from CI pipelines). The consequence for GitHub Actions is a hard boundary. With an automated sync policy configured, pipelines no longer need direct access to the Argo CD API server to perform the deployment, and the pipeline instead makes a commit and push to the repository that tracks the manifests (automated sync policy).

Here is where each piece of work lands and which credential it needs.

StepRuns inCredential it holds
Build the image and push it to the registryGitHub Actions jobShort lived registry credential from OpenID Connect
Resolve the immutable digest of what was pushedGitHub Actions jobNone
Rewrite the image reference in the manifest and commit itGitHub Actions jobGit write token scoped to the config repository
Detect the new commitArgo CD repo server in the clusterArgo CD's own repository credential
Apply the manifests and reconcile driftArgo CD application controllerIn-cluster service account
Report synced and healthyArgo CD API serverRead scoped project token, used only by the verify job

Two facts about that middle row decide how the pipeline feels. Argo CD polls repositories every three minutes to detect changes to the manifests, and the API server can be configured to receive webhook events from the git provider to remove that delay (webhook configuration). Argo CD's own guidance is to keep the Kubernetes manifests in a repository separate from the application source (best practices), which means the commit step usually writes to a second repository and therefore needs a credential that GITHUB_TOKEN cannot provide.

The GitHub Actions side of this is three Linux jobs. All three jobs here belong on Linux; the label catalog is on the cloud runners page. The pipeline that applies manifests directly with kubectl or helm instead, and the private cluster reach it needs, is covered on Kubernetes deploy jobs on GitHub Actions.

Configuration

The workflow below has a build job and a promote job. The build job produces a digest. The promote job checks out the config repository, rewrites the image reference, and pushes. Nothing in either job can talk to the cluster.

name: release-api

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: warp-ubuntu-latest-x64-4x
    outputs:
      image: ${{ steps.ecr.outputs.registry }}/api@${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-actions-build
          aws-region: us-east-1

      - uses: aws-actions/amazon-ecr-login@v2
        id: ecr

      - name: Build and push
        id: push
        uses: Warpbuilds/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}
          profile-name: "api-images-x64"

  promote:
    needs: build
    runs-on: warp-ubuntu-latest-x64-2x
    steps:
      - uses: actions/checkout@v4
        with:
          repository: acme/cluster-config
          ref: main
          ssh-key: ${{ secrets.CONFIG_REPO_DEPLOY_KEY }}
          persist-credentials: true

      - name: Point the overlay at the new digest
        working-directory: overlays/production
        run: kustomize edit set image acme/api=${{ needs.build.outputs.image }}

      - name: Commit and push
        run: |
          git config user.name "release-bot"
          git config user.email "[email protected]"
          git commit -am "api: ${{ needs.build.outputs.image }}"
          for attempt in 1 2 3; do
            git pull --rebase && git push && exit 0
            sleep 5
          done
          exit 1

Four details in that file carry the design.

The promote job consumes a digest through a job output, so the manifest records the exact bytes that were built and a retag upstream cannot change what the cluster runs. What an image digest identifies covers why that reference is stable, and build once and deploy the same artifact everywhere covers the promotion pattern this enables across environments.

kustomize edit set image is the manifest edit Argo CD's own pipeline documentation uses, alongside a kubectl patch --local form for plain YAML (automation from CI pipelines). Either one keeps the commit to a single line of change, which keeps the diff reviewable and the revert trivial.

The push is retried on a rebase. Every service in the fleet writes to the same config repository, so two releases finishing seconds apart will collide on a non fast-forward push, and the loop above is the cheapest fix.

The credential is a deploy key scoped to the config repository alone. The workflow-level permissions block stays at contents: read, and no step receives a token that can write to the source repository (automatic token authentication). Each WarpBuild runner is a virtual machine created for one job and destroyed when the job ends, with its own encrypted storage volume, and WarpBuild does not access or store build secrets (security documentation), so the deploy key lives as long as the promote job does.

Waiting for the rollout and reporting back

A push that nobody watches is a deployment nobody has confirmed. The verify job below downloads the argocd CLI from the API server, which keeps the client version aligned with the server, and blocks until the application is both synced and healthy (argocd app wait).

  verify:
    needs: [build, promote]
    runs-on: warp-ubuntu-latest-x64-2x
    env:
      ARGOCD_SERVER: argocd.internal.acme.example
      ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_PROJECT_TOKEN }}
    steps:
      - name: Install the CLI from the API server
        run: |
          curl -sSL -o /usr/local/bin/argocd \
            https://$ARGOCD_SERVER/download/argocd-linux-amd64
          chmod +x /usr/local/bin/argocd

      - name: Wait for the rollout
        run: argocd app wait api --sync --health --timeout 600

      - name: Report the deployed digest
        if: always()
        run: |
          echo "### api rollout" >> $GITHUB_STEP_SUMMARY
          echo "image: ${{ needs.build.outputs.image }}" >> $GITHUB_STEP_SUMMARY
          argocd app get api -o json \
            | jq -r '"sync: \(.status.sync.status)  health: \(.status.health.status)"' \
            >> $GITHUB_STEP_SUMMARY

The token in ARGOCD_AUTH_TOKEN is a project scoped JWT, and Argo CD RBAC decides what it may read (RBAC configuration). A read scoped token is enough for app wait and app get, so the verify job observes the rollout without gaining the ability to trigger one. Writing the result to $GITHUB_STEP_SUMMARY puts the deployed digest and the final sync and health status on the run page, which is where anyone investigating a bad release looks first.

Sizing

Three jobs with three different shapes. Rates are from the pricing page, billed per minute, checked on 2026-08-13.

LabelvCPURAMStorageUSD per minuteJob it fits
warp-ubuntu-latest-x64-2x28 GB150GB SSD$0.004Promote and verify
warp-ubuntu-latest-x64-4x416 GB150GB SSD$0.008Image build for a single service
warp-ubuntu-latest-x64-8x832 GB150GB SSD$0.016Image build with a heavy compile stage
warp-ubuntu-latest-arm64-4x416 GB150GB SSD$0.006The same build when the image publishes an arm64 manifest

The promote job runs a checkout, a one line file edit, and a push. It is a 2 vCPU job at every fleet size, because the config repository does not grow with the number of services deploying to it. The verify job spends nearly all of its runtime blocked on the Argo CD API, so it belongs on the same row for the same reason.

Worked model for a release pipeline

Assumptions: 12 services, each merging to main and releasing on average once a weekday, 22 weekdays a month, so 264 releases. Each build job holds the runner 6 minutes, each promote job 1 minute, and each verify job 4 minutes of waiting. Substitute your own counts from the workflow run history.

LineArithmeticMonthly
Build jobs264 x 6 min x $0.008$12.67
Promote jobs264 x 1 min x $0.004$1.06
Verify jobs264 x 4 min x $0.004$4.22
Total$17.95

The build line is the one with a GitHub-hosted equivalent. Those 1,584 build minutes list at $0.012 per minute on the 4-core Linux larger runner, which is $19.01 (GitHub Actions billing reference, checked on 2026-08-13). Stated as list price arithmetic: warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) costs $0.008 per minute against $0.012 per minute for the 4-core Linux larger runner (4 vCPU, 16 GB): 33 percent lower list price. GitHub list price checked on 2026-08-13.

The verify line is worth reading twice. It buys nothing except confirmation, it grows with rollout duration rather than with build complexity, and it is the first line that inflates when someone raises a timeout after a flaky release.

Bottlenecks

The polling window sits inside billed runner time

Argo CD polls every three minutes by default, so a verify job that starts the instant the promote job finishes can spend most of its life waiting for the controller to notice a commit it already has. Configure the git webhook to the Argo CD API server and that window collapses to the round trip (webhook configuration). Until then, the timeout on argocd app wait needs headroom for polling plus the rollout itself.

A commit made by GITHUB_TOKEN starts nothing

Events triggered by the repository's GITHUB_TOKEN do not create a new workflow run (triggering a workflow from a workflow). Teams that keep manifests in the same repository and expect the promote commit to start a validation workflow find that nothing fires. A deploy key or GitHub App installation token restores that behavior, and it is also the credential a separate config repository requires. Treat it as a write path into production configuration and scope it to that one repository.

Concurrent promotions collide on one file

Twelve services pushing to one config repository will race. The rebase retry in the promote job above absorbs the common case. Splitting manifests so each service owns its own overlay file keeps the rebases textual rather than semantic, which is what stops a retry from silently reverting another service's release.

Healthy means whatever the health check says

Argo CD assesses health per resource kind and supports custom health checks for resources it does not understand natively (resource health). A custom resource with no health definition can report healthy the moment it is applied, which turns argocd app wait into a check that the manifest arrived. Sync phases and waves also extend the wait, because later waves start only after earlier ones report healthy (sync phases and waves). Both are reasons the verify job's timeout is a deployment decision rather than a formality.

Floating tags quietly break the model

A manifest that references api:latest gives Argo CD nothing to diff, so the commit that was supposed to trigger a rollout changes no bytes in the repository and the controller has no work to do. Digest references are what make the commit meaningful. The alternative path, where a component watches the registry and writes the update itself rather than the pipeline doing it, is Argo CD Image Updater, and it moves the same decision out of GitHub Actions rather than removing it.

What a bigger runner does not fix

A manifest the API server rejects, a readiness probe pointed at the wrong port, or a project RBAC rule that denies the sync fails the same way on every runner size. Faster machines shorten the loop on those failures without preventing them.

Proof

Public repositories running warp- labels show these job shapes in production and the workflow file is one click away. kintsugi-tax/killbill-kintsugi-plugin runs its release workflow on warp-ubuntu-latest-x64-2x, which is the small-runner shape the promote and verify jobs use here (checked on 2026-08-13). triggerdotdev/trigger.dev runs its end-to-end matrix on warp-ubuntu-latest-x64-4x and warp-windows-latest-x64-8x, the build-sized row in the table above (checked on 2026-08-13). Both labels come from the catalog on the cloud runners page.

Every cost figure above carries its arithmetic, a source link, and a checked-on date, and the same per-minute rates appear on the pricing page. Argo CD behavior on this page is cited to the upstream documentation rather than restated from memory, so re-check those links before quoting the polling interval or the wait semantics internally.

FAQ

Does an Argo CD pipeline need cluster credentials in GitHub Actions?

No. With an automated sync policy the workflow pushes a manifest commit to the config repository and the Argo CD controller inside the cluster applies it, so the pipeline needs no direct access to the Argo CD API server and no kubeconfig. The credentials the workflow does hold are a registry credential for the image push and a git write token scoped to the config repository.

How long after the workflow pushes does Argo CD deploy the change?

Argo CD polls repositories every three minutes to detect manifest changes, so an unassisted push waits up to that long before the controller starts syncing. Configuring the API server to receive webhook events from the git provider removes the polling delay, which matters when a GitHub Actions job is billed per minute while it waits for the rollout.

What does an Argo CD release pipeline cost on WarpBuild runners?

The image build job runs on warp-ubuntu-latest-x64-4x at $0.008 per minute, and the manifest commit and rollout verification jobs run on warp-ubuntu-latest-x64-2x at $0.004 per minute, billed per minute of runtime.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.