Answers
Quick Questions
1. What is the main difference between Continuous Integration and Continuous Deployment?
CI automatically builds and tests code changes. CD automatically deploys tested code to production.
2. Where do you place GitHub Actions workflow files in a repository?
In the .github/workflows/ directory with .yml or .yaml extensions.
3. What is a runner in GitHub Actions?
A virtual machine that executes your workflow jobs. GitHub provides Ubuntu, Windows, and macOS runners.
4. How do you make a job wait for another job to complete?
Use the needs keyword:
jobs:
test:
needs: build
runs-on: ubuntu-latest
5. What is the purpose of caching in CI/CD pipelines?
To speed up builds by storing and reusing dependencies between workflow runs.
6. How do you securely store API keys and passwords in GitHub Actions?
Use GitHub Secrets in repository settings, then access with ${{ secrets.SECRET_NAME }}.
7. What does a matrix strategy allow you to do?
Run the same job across multiple configurations (Python versions, OS, etc.) in parallel.
8. Why is `uv` preferred over `pip` for Python CI/CD?
It’s 10-100x faster for dependency installation and provides better dependency locking.
9. What is the “fail fast” principle in CI/CD?
Stop the pipeline immediately when critical issues are detected to provide quick feedback.
10. How do you trigger a workflow only when specific files change?
Use path filters in the workflow trigger:
on:
push:
paths: ['src/**', 'tests/**']
Tasks
Task 1: Hello World
hello.py:
print("Hello CI/CD!")
.github/workflows/hello.yml:
name: Hello World
on: push
jobs:
hello:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python hello.py
Task 2: Python Setup
fetch_data.py:
import requests
response = requests.get('https://api.github.com/zen')
print(f"GitHub Zen: {response.text}")
requirements.txt:
requests
.github/workflows/python-setup.yml:
name: Python Setup
on: push
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: python fetch_data.py
Task 3: Code Quality Check
bad_code.py:
import os,sys
def calc(x,y):
return x/y
print(calc(10,2))
.github/workflows/quality.yml:
name: Code Quality
on: push
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install ruff
- run: ruff check .
- run: ruff format --check .
Fix the code by properly formatting it and ruff will pass.
Task 4: Running Tests
calculator.py:
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
test_calculator.py:
import pytest
from calculator import add, divide
def test_add():
assert add(2, 3) == 5
def test_divide():
assert divide(10, 2) == 5
def test_divide_by_zero():
with pytest.raises(ValueError):
divide(10, 0)
requirements.txt:
pytest
.github/workflows/test.yml:
name: Tests
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install -r requirements.txt
- run: pytest -v
Task 5: Using Secrets
api_client.py:
import os
import requests
token = os.environ['GITHUB_TOKEN']
headers = {'Authorization': f'token {token}'}
response = requests.get('https://api.github.com/user', headers=headers)
print(f"User: {response.json()['login']}")
.github/workflows/secrets.yml:
name: Using Secrets
on: push
jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install requests
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python api_client.py
Set up GITHUB_TOKEN in repository Settings → Secrets.
Task 6: Caching Dependencies
data_app.py:
import pandas as pd
data = {'name': ['Alice', 'Bob'], 'age': [25, 30]}
df = pd.DataFrame(data)
print(df)
requirements.txt:
pandas
.github/workflows/caching.yml:
name: Caching Demo
on: push
jobs:
process:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- run: pip install -r requirements.txt
- run: python data_app.py
Task 7: Matrix Testing
version_check.py:
import sys
version = sys.version_info
print(f"Python {version.major}.{version.minor}")
if version >= (3, 12):
print("Has new f-string features")
.github/workflows/matrix.yml:
name: Matrix Testing
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: python version_check.py
Task 8: Environment Variables
config_app.py:
import os
env = os.environ.get('APP_ENV', 'dev')
debug = os.environ.get('DEBUG', 'false') == 'true'
print(f"Environment: {env}")
print(f"Debug mode: {debug}")
if env == 'production':
print("Running in production!")
else:
print("Running in development")
.github/workflows/environments.yml:
name: Environment Config
on: push
jobs:
dev:
runs-on: ubuntu-latest
env:
APP_ENV: development
DEBUG: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python config_app.py
prod:
runs-on: ubuntu-latest
env:
APP_ENV: production
DEBUG: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python config_app.py
Task 9: Branch Protection
.github/workflows/pr.yml:
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install ruff pytest
- run: ruff check .
- run: pytest
Set up branch protection in Settings → Branches → Add rule for main:
Require status checks to pass
Select “quality” as required check
Require pull request reviews
Task 10: Deployment Pipeline
pyproject.toml:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-demo-package"
version = "0.1.0"
description = "Demo package"
src/my_package/__init__.py:
def hello():
return "Hello from my package!"
.github/workflows/deploy.yml:
name: Deploy
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install build twine
- run: python -m build
- env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TESTPYPI_API_TOKEN }}
run: twine upload --repository testpypi dist/*
Create a release on GitHub to trigger deployment.
Open-Ended Questions
Question 1: Pipeline Strategy
For a 10-person team:
Keep pipeline under 10 minutes total
Run tests in parallel by module
Use caching for dependencies
Automatic staging deployment, manual production
Set up notifications for failures
Question 2: Testing Strategy
For API with external services:
70% unit tests (mocked externals)
25% integration tests (test doubles)
5% end-to-end tests (real services)
Test error scenarios and timeouts
Use contract testing for API compatibility
Question 3: Security Integration
To maintain development speed:
Run basic security checks in PR pipeline (< 2 minutes)
Full security scans nightly
Auto-fix simple vulnerabilities
Block on critical issues, alert on medium/low
Integrate security training in onboarding
Question 4: Multi-Environment Strategy
Environment management:
Use environment variables for config differences
Promote through: dev → staging → production
Automatic deployment to dev/staging
Manual approval for production
Keep environments as similar as possible
Question 5: Tool Selection
Evaluation criteria:
Start with team’s existing tools and skills
Consider integration with current workflow
Evaluate setup time and learning curve
Test with a pilot project
Consider long-term maintenance needs
Balance features with simplicity