LESSON 84 · ADVANCED4 min read

CI/CD Pipelines

Automate a clean test run, build one immutable artifact, and deploy it with explicit permissions and a rollback path.

Python 3.12+Free to readModule 19 of 19
YOUR NEXT SMALL WINS

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.

.github/workflows/test.ymlYAML
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-missing

02Build 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.

.github/workflows/publish.ymlYAML
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.ymlYAML
permissions:  contents: readjobs:  test:    permissions:      contents: read  deploy:    permissions:      contents: read      id-token: write   # required for OIDC federation

04Deploy 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.

health_gate.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyYAML
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 pytest

The workflow is deliberately small: checkout, interpreter, dependencies, tests. Add caching and coverage only after this baseline is reliable.

02

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.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyYAML
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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01Why build the deployable artifact in CI?
02What is the purpose of an explicit permissions block?
03What should precede removing an old database column?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
[Process exited with code 0 - No output]
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

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.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.