RizTech Academy logo
RizTech Academy
CI/CDLesson 4 of 635 min

Adding a deployment stage

CI tests your code and builds an image; CD takes that image and actually gets it running. This lesson adds the deployment half of the pipeline: push the image to a registry, then deploy it — and, crucially, only if the tests passed. By the end you have the full path the module promised: a push leads automatically to tested, deployed software. This lesson is adding a deployment stage.

The deployment stage, and the gate between

Deployment is a separate job that runs after the test job and only if it succeeded. That ordering is the whole safety of CD:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npm test

  deploy:
    needs: build-and-test        # THE GATE: only runs if tests passed
    if: github.ref == 'refs/heads/main'   # and only for main, not every PR
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "build and push image, then deploy..."   # the real steps below

Two guards make this safe:

  • needs: build-and-test — the deploy job does not even start unless the test job succeeded. Broken code cannot deploy. This is the gate (the why-ci lesson) expressed across jobs.
  • if: github.ref == 'refs/heads/main' — deploy only on the main branch, not on every pull request. You test every PR, but you deploy only what has merged to main.

Together: test everything, deploy only what passed and merged. That is continuous deployment in one pattern.

Step 1: build and push the image to a registry

To deploy an image, the servers must be able to pull it, so the pipeline pushes it to a container registry (Docker Hub, GitHub Container Registry, Amazon ECR). The deploy job builds the image (tagged by commit — the build-and-test lesson) and pushes it:

      - name: Log in to the registry
        run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
      - name: Build and push
        run: |
          docker build -t ghcr.io/riztech/greetings:${{ github.sha }} .
          docker push ghcr.io/riztech/greetings:${{ github.sha }}

Note secrets.REGISTRY_TOKEN — the login credential comes from a secret, never hard-coded (the next lesson). Now the image, tagged with this exact commit, is in the registry where the deployment target can pull it.

Step 2: deploy the image

How you deploy depends on where you run. Since this course targets Kubernetes (the previous module), deploying means telling the cluster to run the new image — which, because Kubernetes is declarative, is just updating the Deployment to the new image tag:

      - name: Deploy to Kubernetes
        run: |
          kubectl set image deployment/greetings greetings=ghcr.io/riztech/greetings:${{ github.sha }}
          kubectl rollout status deployment/greetings

kubectl set image updates the Deployment's desired state to the new image; Kubernetes then performs a rolling update (the scaling-and-health lesson) — new pods up, old pods down, gated by readiness, zero downtime. kubectl rollout status waits for the rollout to finish, so the pipeline fails if the deploy fails — you find out in the pipeline, not from users. (Other targets differ — a platform like Vercel deploys on push, a VM might pull-and-restart — but the shape is the same: get the new image running where it runs.)

The pipeline now does the full journey: push → test → build & push image → deploy with a rolling update → verify the rollout — automatically, on every merge to main, with a gate that stops it if anything fails.

Environments: staging before production

One more practice that real pipelines use: deploy to staging first, then production. You do not push straight to production on every merge (usually); you deploy to a staging environment that mirrors production, verify there (automated checks, maybe a manual look), and then promote to production:

  deploy-staging:
    needs: build-and-test
    steps: [ ... deploy to staging ... ]
  deploy-production:
    needs: deploy-staging
    environment: production      # can require a manual approval
    steps: [ ... deploy to production ... ]

GitHub environments let you add protection to a deploy job — notably a required manual approval before production, so a human confirms the release. This is the difference between continuous delivery (automated up to a one-click production release) and continuous deployment (fully automated to production): the pipeline is the same; whether the final production step needs a human is a policy choice. Staging-then-production, with an approval gate on production, is a very common and sensible default.

Check your work

The deploy job runs after tests, guarded by needs: build-and-test (won't start unless tests passed — the gate across jobs) and if: github.ref == 'refs/heads/main' (deploy only merged code, not every PR). Test everything, deploy only what passed and merged.

Push the image: log in to a registry (Docker Hub/GHCR/ECR) using a secret (never hard-coded), build the commit-tagged image, and push — so the target can pull it.

Deploy it: on Kubernetes, kubectl set image deployment/... =<new tag> updates desired state → a rolling update (zero downtime); kubectl rollout status makes the pipeline fail if the deploy fails (you find out, not users). Other targets differ in mechanics, same shape.

Environments: deploy to staging first, verify, then production — with GitHub environments able to require a manual approval before prod. Continuous delivery (one-click prod) vs deployment (fully automated) is just whether that final step needs a human.

Practice

  1. Write a deploy job gated by needs: and restricted to main with if:; explain both guards.
  2. Explain why the image must be pushed to a registry before it can be deployed.
  3. Explain how kubectl set image + kubectl rollout status deploys with zero downtime and fails safely.
  4. Explain why the login token comes from secrets. rather than being written in the workflow.
  5. Design a staging-then-production flow with a manual approval before production.
  6. Explain the difference between continuous delivery and continuous deployment in terms of that final step.

Official documentation

Next: deployment strategies — rolling, blue-green, canary and rollback.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship