CI/CD Pipelines
Automate a clean test run, build one immutable artifact, and deploy it with explicit permissions and a rollback path.
By the end, you’ll be able to…
- Write a CI workflow that reproduces a clean environment.
- Build once and promote the same artifact.
- Store secrets and grant permissions deliberately.
- Plan health checks, migrations, and rollback before deployment.
01CI reproduces a clean environment
Continuous integration runs the project's checks on a fresh machine for every push or pull request. Checkout, install the supported Python version, install dependencies, and run the same test command used locally. If the workflow needs a manual step that is not documented, it is not reproducible.
Keep the workflow fast enough to be used. Cache dependencies with a lock-file key, run independent jobs in parallel, and fail early on formatting or type errors. A slow pipeline invites bypasses; a clear, quick one becomes the normal path.
name: teston: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip - run: python -m pip install -r requirements.txt - run: python -m pytest --cov=app --cov-report=term-missing02Build once, promote the artifact
A deployment should use the exact artifact that passed tests. Build a container image in CI, tag it with the commit SHA, push it to a registry, and deploy that tag. Rebuilding from source at deploy time creates a new artifact that no test has seen.
Separate the test job from the publish job and make publish depend on test. Use least-privilege credentials for the registry. Keep the image digest in the deployment record so a rollback can return to an exact previous artifact, not a mutable latest tag.
name: publishon: push: branches: [main]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: python -m pip install -r requirements.txt - run: python -m pytest publish: needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} . - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}03Secrets and permissions
Store secrets in the CI platform's secret store, inject them only into the jobs that need them, and never print them. A secret echoed in a log is compromised even if the job later fails. Prefer short-lived identity tokens over long-lived cloud keys when the platform supports OIDC.
Set an explicit permissions block on the workflow. The default token often has more access than a test job needs. A test job usually needs read-only contents; a publish job needs package write. Scope permissions per job rather than granting the widest set to the whole workflow.
permissions: contents: readjobs: test: permissions: contents: read deploy: permissions: contents: read id-token: write # required for OIDC federation04Deploy with a rollback plan
A deployment is not finished when the process starts. Run a health check, watch error rates for a defined window, and know how to return to the previous artifact. Database migrations need special care: expand the schema, deploy compatible code, backfill, and only then remove the old column.
Blue-green and canary deployments reduce risk by shifting traffic gradually. Blue-green runs two environments and switches when the new one is healthy; canary sends a small share of traffic first. Both require observability and an automated or rehearsed rollback. A rollback plan that has never been tested is a hope, not a plan.
The ideas worth keeping.
- CI should reproduce the commands a developer can run locally.
- Build one artifact, tag it immutably, and promote that artifact.
- Deploy with health checks, observability, and a rehearsed rollback.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Write a test workflow
Create a GitHub Actions workflow triggered by pushes and pull requests that installs Python 3.12, installs requirements, and runs pytest.
Explore a worked solution
name: teston: [push, pull_request]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: python -m pip install -r requirements.txt - run: python -m pytestThe workflow is deliberately small: checkout, interpreter, dependencies, tests. Add caching and coverage only after this baseline is reliable.
Tag an image by commit
Write the build and push steps for an image tagged with the commit SHA, using the workflow's built-in token and a package-write permission.
Explore a worked solution
permissions: contents: read packages: writesteps: - uses: actions/checkout@v4 - run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} . - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}The SHA tag identifies the exact source revision. The deployment record can refer to this immutable tag, and a rollback can select a previous SHA.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Write a test workflow
Create a GitHub Actions workflow triggered by pushes and pull requests that installs Python 3.12, installs requirements, and runs pytest.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.