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 themainbranch, not on every pull request. You test every PR, but you deploy only what has merged tomain.
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
- Write a
deployjob gated byneeds:and restricted tomainwithif:; explain both guards. - Explain why the image must be pushed to a registry before it can be deployed.
- Explain how
kubectl set image+kubectl rollout statusdeploys with zero downtime and fails safely. - Explain why the login token comes from
secrets.rather than being written in the workflow. - Design a staging-then-production flow with a manual approval before production.
- Explain the difference between continuous delivery and continuous deployment in terms of that final step.
Official documentation
- GitHub Actions — Deploying with GitHub Actions — Building deployment pipelines.
- GitHub Actions — Using environments for deployment — Staging, production and manual approvals.
- Kubernetes — Deployments (rolling update) — What
kubectl set imagetriggers.
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