GitHub Actions CI/CD (4/10): Your First CI Workflow — Run on Every PR
Summary: Create a GitHub Actions workflow that automatically checks out your code, installs the package, runs lint, and runs tests on every push and pull request. This is the moment your local checks become enforced CI.
| Key | Value |
|---|---|
| Package name | helloci |
| Python version | 3.12 |
| Working directory | ~/projects/helloci |
| Workflow file | .github/workflows/ci.yml |
| Trigger events | push, pull_request |
| Runner | ubuntu-latest |
0. Prerequisites
- The
hellocipackage with tests and ruff from Parts 1–3 - A GitHub account
gitinstalled locally./check.shpasses locally (lint + format + tests)
1. Initialize a Git Repository
If you have not already initialized git in your project, do it now.
cd ~/projects/helloci
git initCode language: Shell Session (shell)
Create a .gitignore file to keep build artifacts and virtual environments out of the repository.
__pycache__/
*.egg-info/
dist/
build/
.venv/
*.pycCode language: Shell Session (shell)
Stage and commit everything.
git add .
git commit -m "Initial commit: helloci package with tests and lint"Code language: Shell Session (shell)
2. Push to GitHub
Create a new repository on GitHub called helloci. Do not add a README or .gitignore — your local project already has everything it needs.
Add the remote and push.
git remote add origin https://github.com/<YOUR_GITHUB_USERNAME>/helloci.git
git branch -M main
git push -u origin mainCode language: Shell Session (shell)
Replace <YOUR_GITHUB_USERNAME> with your actual GitHub username.
3. Create the Workflow Directory
GitHub Actions looks for workflow files in a specific directory.
mkdir -p .github/workflowsCode language: Shell Session (shell)
This path is not optional — .github/workflows/ is where GitHub discovers your workflow files.
4. Write the Workflow File
Create .github/workflows/ci.yml.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install package and test dependencies
run: pip install -e ".[test]"
- name: Lint
run: ruff check .
- name: Check formatting
run: ruff format . --check
- name: Run tests
run: pytest -vCode language: YAML (yaml)
This is the entire workflow. Save the file.
5. Understand the Workflow
Every line in that file does something specific. Here is what each section means.
Trigger block:
on:
push:
branches: [main]
pull_request:
branches: [main]Code language: YAML (yaml)
The workflow runs when code is pushed to main or when a pull request targets main. These are called trigger events.
Job definition:
jobs:
check:
runs-on: ubuntu-latestCode language: YAML (yaml)
A job is a set of steps that run on a fresh virtual machine. ubuntu-latest tells GitHub to use the most recent Ubuntu runner. Each job starts from a clean state — no leftover files from previous runs.
Steps:
| Step | What it does |
|---|---|
actions/checkout@v4 | Clones your repository onto the runner |
actions/setup-python@v5 | Installs the specified Python version |
pip install -e ".[test]" | Installs your package and test dependencies |
ruff check . | Runs the linter |
ruff format . --check | Checks code formatting |
pytest -v | Runs all unit tests |
Steps run top to bottom. If any step fails (non-zero exit code), the entire job stops and is marked as failed. This is why the check.sh script from Part 3 used set -euo pipefail — same principle.
Note:
actions/checkout@v4andactions/setup-python@v5are actions — reusable packages maintained by GitHub. The@v4part is a version tag. Always pin to a major version.
6. Push and Watch It Run
Commit the workflow file and push.
git add .github/workflows/ci.yml
git commit -m "Add CI workflow: lint, format, tests"
git pushCode language: Shell Session (shell)
Open your repository on GitHub. Click the Actions tab. You should see your workflow running.
The workflow appears as a green checkmark (passed) or red X (failed) next to the commit. Click the workflow run to see the full log.
7. Read the Workflow Logs
Click into the workflow run, then click the check job. You see each step with a collapsible log.
The log output looks exactly like what you see locally when you run ruff check . and pytest -v. This is by design — CI runs the same commands you run on your machine.
If a step fails, the log shows the error message. For example, if a test fails, you see the same pytest output with the assertion error and the failing line.
Tip: If the workflow fails and you cannot figure out why, compare the CI log to your local output. The most common cause is a missing dependency — something installed locally but not listed in
pyproject.toml.
8. Test the Workflow With a Pull Request
Create a branch, make a small change, and open a pull request.
git checkout -b test-ciCode language: Shell Session (shell)
Edit src/helloci/__init__.py — add a second function.
def farewell(name: str) -> str:
"""Return a farewell string."""
return f"Goodbye, {name}!"Code language: Python (python)
Commit and push the branch.
git add src/helloci/__init__.py
git commit -m "Add farewell function"
git push -u origin test-ciCode language: Shell Session (shell)
Open a pull request on GitHub from test-ci into main. The CI workflow runs automatically. On the pull request page, you see the check status — either a green checkmark or a red X next to the workflow name.
Warning: This pull request is just for testing. You can merge it or close it — the point is to see CI run on a PR.
9. Verify Your Project Structure
Your project should now look like this.
~/projects/helloci/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── check.sh
├── pyproject.toml
├── src/
│ └── helloci/
│ ├── __init__.py
│ └── cli.py
└── tests/
├── __init__.py
├── test_cli.py
└── test_greet.pyCode language: Shell Session (shell)
Summary
You created a GitHub Actions workflow that runs lint, format checks, and tests on every push and pull request. The workflow file lives at .github/workflows/ci.yml and uses four key actions and commands.
- Trigger: push to
mainand pull requests targetingmain - Runner:
ubuntu-latest— a fresh VM for every run - Steps: checkout, setup Python, install, lint, format check, test
- Feedback: green checkmark or red X on every commit and PR
Your code now has an automated quality gate. Nobody (including you) can push broken code to main without seeing a red X. In Part 5 you will expand this workflow to test across multiple Python versions.
GitHub Actions CI/CD — All Parts
- 1 GitHub Actions CI/CD (1/10): Make It a Package You Can Test
- 2 GitHub Actions CI/CD (2/10): Unit Tests — Fast Feedback
- 3 GitHub Actions CI/CD (3/10): Quality Gate Before Tests — Lint and Formatting
- 4 GitHub Actions CI/CD (4/10): Your First CI Workflow — Run on Every PR You are here
- 5 GitHub Actions CI/CD (5/10): Matrix Testing — Multiple Python Versions
- 6 GitHub Actions CI/CD (6/10): Integration Tests — Real Dependencies in CI
- 7 GitHub Actions CI/CD (7/10): Branch Protection — Make CI a Merge Gate
- 8 GitHub Actions CI/CD (8/10): Artifacts — Keep Evidence From a Run
- 9 GitHub Actions CI/CD (9/10): Release Workflow — Tag, Build, Publish
- 10 GitHub Actions CI/CD (10/10): Going Professional — Split Jobs, Caching, and Nightly Builds
