Homework — Push-to-Deploy
Lesson 06 · Cloud Infrastructure & DevOps · 3 tasks
Task 1 — Add a staging environment
Create a second GitHub Environment named staging (no required reviewer — staging deploys should be fast). Modify deploy.yml to add a deploy-staging job that runs before deploy-production. The production job should only start if the staging deploy succeeds and its healthcheck passes.
The two-job structure should look like this (fill in the steps):
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.shortener.example.com
steps:
# 1. SSH to staging VM, pull, up -d
# 2. Smoke-test: curl -fsS https://staging.shortener.example.com/healthz
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://shortener.example.com
steps:
# same as lab deploy steps
Create a corresponding staging environment in GitHub Settings → Environments. Add STAGING_HOST, STAGING_USER, and STAGING_KEY secrets to the staging environment (you can reuse the same VM from the lab, or create a second one).
Self-check: push to main. The Deploy workflow runs two jobs. deploy-staging starts immediately (no approval required). deploy-production starts only after staging passes and only after the production reviewer approves. The staging URL returns 200 on /healthz before the production approval prompt fires.
Task 2 — Pin deploys to the digest, not the tag
Modify deploy.yml so the SSH step receives the image digest rather than the commit SHA tag. The deploy target’s docker-compose.yml should reference the image by digest (ghcr.io/.../api@sha256:...), making the deployed version immutable and traceable.
You will need to pass the digest from the Security & Sign workflow to the deploy job. One reliable approach: in security.yml, upload the digests as a workflow artifact at the end of the sign job. In deploy.yml, download that artifact using actions/download-artifact@v4 with the run-id set to github.event.workflow_run.id.
A sketch of the artifact approach:
# In security.yml, at the end of the sign job:
- name: Save digests
run: |
echo "api=${{ steps.api.outputs.digest }}" > digests.env
echo "worker=${{ steps.worker.outputs.digest }}" >> digests.env
- uses: actions/upload-artifact@v4
with:
name: image-digests
path: digests.env
# In deploy.yml, before the SSH step:
- uses: actions/download-artifact@v4
with:
name: image-digests
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Load digests
run: cat digests.env >> $GITHUB_ENV
After loading, $api and $worker are available as environment variables in subsequent steps. Pass them to the SSH script and use them to set IMAGE_DIGEST_API and IMAGE_DIGEST_WORKER on the target, then reference them in docker-compose.yml.
Self-check: after a deploy, SSH to the target and run docker compose config. The image: fields for api and worker should end with @sha256: rather than a tag name. Confirm that deploying the same digest twice is a no-op (no container restart).
Task 3 — Concurrent-deploy safety experiment
Push two commits to main in quick succession (e.g., merge two pull requests within 30 seconds of each other). Observe what happens in the Actions tab under the two Deploy workflow runs.
With cancel-in-progress: false (the current setting): both Deploy runs are queued in the deploy-production concurrency group. The second run waits for the first to finish before starting. Neither is cancelled.
Now temporarily change cancel-in-progress: true in deploy.yml, commit, and repeat the two-commit experiment. Observe: the first deploy run is cancelled mid-flight when the second one starts.
Write a short paragraph (3–5 sentences) in your homework writeup documenting both behaviors and the trade-off:
- When is
cancel-in-progress: falsethe safer choice? (hint: think about what state a partially-completed deploy leaves) - When might
cancel-in-progress: truebe acceptable? (hint: think about non-production lanes or pure build jobs)
Self-check: your writeup explains the correctness-vs-speed trade-off in your own words. You have a screenshot from the Actions UI showing the queued behavior (false) and the cancellation behavior (true). Revert to cancel-in-progress: false before submitting.