What you'll learn
Quick Answer
Put a YAML file in .github/workflows and GitHub runs it on every push. Start with just your test suite, add branch protection so failures block merges, and keep secrets in repository settings rather than in the file.
CI and CD, distinguished
Continuous integration means every change is automatically built and tested when pushed. That is the part with the highest value and the lowest cost, and it is where to start.
Continuous delivery means changes that pass are automatically prepared for release. Continuous deployment goes further and releases them without a human.
Most student projects should do CI and stop there. Automatic deployment on every merge is a good way to publish a broken site quickly when your test coverage is thin.
The value of CI is specific: tests run on a clean machine, not yours. That catches the classic failure where code works locally because of a file, an environment variable or an installed package that exists only on your laptop — see virtual environments and pip.
A workflow, line by line
Create .github/workflows/tests.yml:
name: Tests
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: python -m pytest -q
on— when to run. Here, pushes to main and every pull request.runs-on— a fresh virtual machine, discarded afterwards.actions/checkout— clones your repository. Without it the machine is empty; forgetting it is the most common first mistake.run— shell commands, exactly as you would type them.
Push this and the Actions tab shows the run. If any command exits non-zero, the job fails and the commit is marked with a red cross.
Making it genuinely useful
Test several versions at once with a matrix:
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
Three parallel jobs, one per version. Useful for libraries; unnecessary for an application pinned to one runtime.
Cache dependencies, or every run reinstalls everything. setup-python and setup-node both accept a cache option, which often turns a two-minute install into a few seconds.
Add linting as a separate step so formatting problems are caught without argument in review.
Keep it fast. A pipeline taking fifteen minutes stops being waited for, and people merge without checking. Under five minutes is a reasonable target for a student project.
Secrets and permissions
Never put credentials in the workflow file — it is committed and public in a public repository.
Add them under repository settings, then reference them:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
GitHub masks secret values in logs, so an accidental print shows asterisks. That masking is not a licence to be careless, but it prevents the most common leak.
Two cautions. Workflows triggered by pull requests from forks do not receive secrets by default — deliberately, since anyone can open a pull request. And pin third-party actions to a version rather than a moving branch, because you are executing someone else's code on a machine holding your secrets.
Make it matter: branch protection
CI that reports failures nobody has to act on is decoration. The step that makes it real is branch protection.
In repository settings, require status checks to pass before merging into main. Now a pull request with failing tests cannot be merged — the button is disabled.
For a group project this is the single most valuable setting available. It ends the situation where someone pushes broken code on Friday and the team discovers it on Monday, and it removes the social awkwardness of enforcing standards — the rule is automatic and applies to everyone.
Combine with requiring a review, and you have essentially the workflow professional teams use. Being able to say you set that up on a college project is a genuinely good interview answer, because most graduates have never used CI at all.
A status badge in the README is a small extra touch that signals the project is maintained — see what recruiters look at on your GitHub.
