Skip to content

Commit f40160b

Browse files
committed
Overhaul CI lesson
1 parent 9b358f2 commit f40160b

File tree

6 files changed

+246
-74
lines changed

6 files changed

+246
-74
lines changed

episodes/10-CI.Rmd

Lines changed: 246 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Continuous Integration with GitHub Actions"
3-
teaching: 10
4-
exercises: 2
3+
teaching: 20
4+
exercises: 25
55
---
66

77
:::::::::::::::::::::::::::::::::::::: questions
@@ -20,39 +20,61 @@ exercises: 2
2020

2121
## Continuous Integration
2222

23-
Continuous Integration (CI) is the practice of automating the merging of code changes into a project.
24-
In the context of software testing, CI is the practice of running tests on every code change to ensure that the code is working as expected.
25-
GitHub provides a feature called GitHub Actions that allows you to integrate this into your projects.
23+
Continuous Integration (CI) is the practice of automating the merging of code
24+
changes into a project. In the context of software testing, CI is the practice
25+
of running tests on every code change to ensure that the code is working as
26+
expected. GitHub provides a feature called GitHub Actions that allows you to
27+
integrate this into your projects.
2628

27-
In this lesson we will go over the very basics of how to set up a GitHub Action to run tests on your code.
29+
In this lesson we will go over the very basics of how to set up a GitHub Action
30+
to run tests on your code.
31+
32+
:::::: prereq
33+
34+
This lesson assumes a working knowledge of Git and GitHub. If you get stuck,
35+
you may find it helpful to review the Research Coding Course's
36+
[material on version control](https://researchcodingclub.github.io/course/#version-control-introduction-to-git-and-github)
37+
38+
:::::::::::::
2839

2940
## Setting up your project repository
3041

31-
- Create a new repository on GitHub for this lesson called "python-testing-course" (whatever you like really)
32-
- Clone the repository into your local machine using `git clone <repository-url>` or GitKraken if you use that.
42+
- Create a new repository on GitHub for this lesson called
43+
"python-testing-course" (whatever you like really). We
44+
recommended making it public for now.
45+
- Clone the repository into your local machine using `git clone
46+
<repository-url>` or via Github Desktop.
3347
- Move over all your code from the previous lessons into this repository.
3448
- Commit the changes using `git add .` and `git commit -m "Add all the project code"`
35-
- Create a new file called `requirements.txt` in the root of your repository and add the following contents:
49+
- Create a new file called `requirements.txt` in the root of your repository
50+
and add the following contents:
3651

3752
```
3853
pytest
3954
numpy
4055
pandas
41-
pytest-mpl
42-
pytest-regtest
43-
matplotlib
4456
```
4557

46-
This is just a list of all the packages that your project uses and will be needed later.
47-
Recall that each of these are used in various lessons in this course.
58+
This is just a list of all the packages that your project uses and will be
59+
needed later. Recall that each of these are used in various lessons in this
60+
course.
61+
62+
:::::: callout
4863

64+
Nowadays it is usually preferable to list dependencies in a file called
65+
`pyproject.toml`, which also allows Python packages to be installed and
66+
published. Look out for our upcoming course on reproducible environments to
67+
learn more!
68+
69+
::::::::::::::
4970

5071
Now we have a repository with all our code in it online on GitHub.
5172

5273
## Creating a GitHub Action
5374

54-
GitHub Actions are defined in `yaml` files (these are just simple text files that contain a list of instructions). They are stored
55-
in the `.github/workflows` directory in your repository.
75+
GitHub Actions are defined in `yaml` files -- a structured text file which is
76+
commonly used to pass settings to programs. They are stored in the
77+
`.github/workflows` directory in your repository.
5678

5779
- Create a new directory in your repository called `.github`
5880
- Inside the `.github` directory, create a new directory called `workflows`
@@ -66,93 +88,243 @@ Let's add some instructions to the `tests.yaml` file:
6688
# This is just the name of the action, you can call it whatever you like.
6789
name: Tests (pytest)
6890

69-
# This is the event that triggers the action. In this case, we are telling GitHub to run the tests whenever a pull request is made to the main branch.
91+
# This sets the events that trigger the action. In this case, we are telling
92+
# GitHub to run the tests whenever a push is made to the repository.
93+
# The trailing colon is intentional!
7094
on:
71-
pull_request:
72-
branches:
73-
- main
95+
push:
7496

75-
# This is a list of jobs that the action will run. In this case, we have only one job called build.
97+
# This is a list of jobs that the action will run. In this case, we have only
98+
# one job called build.
7699
jobs:
77-
build:
78-
# This is the environment that the job will run on. In this case, we are using the latest version of Ubuntu, however you can ues other operating systems like Windows or MacOS if you like!
79-
runs-on: ubuntu-latest
80-
81-
# This is a list of steps that the job will run. Each step is a command that will be executed on the environment.
82-
steps:
83-
# This command tells GitHub to use a pre-built action. In this case, we are using the actions/checkout action to check out the repository. This just means that GitHub will use this repository's code to run the tests.
84-
- uses: actions/checkout@v3 # Check out the repository on github
85-
# This is the name of the step. This is just a label that will be displayed in the GitHub UI.
86-
- name: Set up Python 3.10
87-
# This command tells GitHub to use a pre-built action. In this case, we are using the actions/setup-python action to set up Python 3.10.
88-
uses: actions/setup-python@v3
89-
with:
90-
python-version: "3.10"
91-
92-
# This step installs the dependencies for the project such as pytest, numpy, pandas, etc using the requirements.txt file we created earlier.
93-
- name: Install dependencies
94-
run: |
95-
python -m pip install --upgrade pip
96-
pip install -r requirements.txt
97-
98-
# This step runs the tests using the pytest command. Remember to use the --mpl and --regtest flags to run the tests that use matplotlib and pytest-regtest.
99-
- name: Run tests
100-
run: |
101-
pytest --mpl --regtest
100+
101+
build:
102+
103+
# This is the environment that the job will run on. In this case, we are
104+
# using the latest version of Ubuntu, however you can use other operating
105+
# systems like Windows or MacOS if you like!
106+
runs-on: ubuntu-latest
107+
108+
# This is a list of steps that the job will run. Each step is a command
109+
# that will be executed on the environment.
110+
steps:
111+
112+
# This command tells GitHub to use a pre-built action. In this case, we
113+
# are using the actions/checkout action to check out the repository. This
114+
# just means that GitHub will clone this repository to the current
115+
# working directory.
116+
- uses: actions/checkout@v6
117+
118+
# This is the name of the step. This is just a label that will be
119+
# displayed in the GitHub UI.
120+
- name: Set up Python 3.12
121+
# This command tells GitHub to use a pre-built action. In this case, we
122+
# are using the actions/setup-python action to set up Python 3.12.
123+
uses: actions/setup-python@v6
124+
with:
125+
python-version: "3.12"
126+
127+
# This step installs the dependencies for the project such as pytest,
128+
# numpy, pandas, etc using the requirements.txt file we created earlier.
129+
- name: Install dependencies
130+
run: |
131+
python -m pip install --upgrade pip
132+
pip install -r requirements.txt
133+
134+
# This step runs the tests using the pytest command.
135+
- name: Run tests
136+
run: |
137+
pytest
102138
```
103139
104-
This is a simple GitHub Action that runs the tests for your code whenever a pull request is made to the main branch.
140+
This is a simple GitHub Action that runs the tests for your code whenever code
141+
is pushed to the repository, regardless of what was changed in the repository
142+
or which branch you push too. We'll see later how to run tests only when
143+
certain criteria are fulfilled.
105144
106145
## Upload the workflow to GitHub
107146
108147
Now that you have created the `tests.yaml` file, you need to upload it to GitHub.
109148

110149
- Commit the changes using `git add .` and `git commit -m "Add GitHub Action to run tests"`
111-
- Push the changes to GitHub using `git push origin main`
150+
- Push the changes to GitHub using `git push`
151+
152+
This should trigger a workflow on the repository. While it's running, you'll see an orange
153+
circle next to your profile name at the top of the repo. When it's done, it'll change to
154+
a green tick if it finished successfully, or a red cross if it didn't.
155+
156+
![GitHub repository view with a green tick indicating a successful workflow run](fig/github_repo_view.png){alt="GitHub repository view with a green tick indicating a successful workflow run"}
157+
158+
You can view all previous workflow runs by clicking the 'Actions' button on the
159+
top bar of your repository.
160+
161+
![GitHub Actions button](fig/github_actions_button.png){alt="GitHub Actions Button"}
162+
163+
If you click on the orange circle/green tick/red cross, you can also view the
164+
individual stages of the workflow and inspect the terminal output.
165+
166+
![Detailed view of a GitHub workflow run](fig/github_action.png){alt="Detailed view of a GitHub workflow run"}
167+
112168

113169
## Enable running the tests on a Pull Request
114170

115-
The typical use-case for a CI system is to run the tests whenever a pull request is made to the main branch to add a feature.
171+
The typical use-case for a CI system is to run the tests when a pull request is
172+
made to the `main` branch to add a feature or fix a bug. However, there is a
173+
subtlety to consider: what if the tests pass on your feature branch, but would
174+
fail after merging into `main` due to a divergence between the two branches?
116175

117-
<!-- figure
118-
![branch protection visual instruction](episodes/fig/github-branch-protection.png) -->
119-
- Go to your GitHub repository
120-
- Click on the "Settings" tab
121-
- Scroll down to "Branches"
122-
- Under "Branch protection rules" / "Branch name pattern" type "main"
123-
- Select the checkbox for "Require status checks to pass before merging"
124-
- Select the checkbox for "Require branches to be up to date before merging"
176+
To verify this, we can set our workflow to run on both `push` and
177+
`pull_request` by modifying the `tests.yaml` file so that the `on:` sections
178+
reads:
125179

126-
This makes it so when a Pull Request is made, trying to merge code into main, it will need to have all of its tests passing
127-
before the code can be merged.
180+
```yaml
181+
on:
182+
push:
183+
pull_request:
184+
```
128185

129186
Let's test it out.
130187

131-
- Create a new branch in your repository called `subtract` using `git checkout -b subtract`
132-
- Add a new function in your `calculator.py` file that subtracts two numbers, but make it wrong on purpose:
188+
- From within your `main` branch, add a new file `sandwich.py` containing the
189+
following function:
133190

134191
```python
135-
def subtract(a, b):
136-
return a + b
192+
def sandwich():
193+
bread = "======"
194+
filling = "jam"
195+
return "\n".join([bread, filling, bread])
137196
```
138197

139-
- Then add a test for this function in your `test_calculator.py` file:
198+
- Create a new branch in your repository called `sanwich_test` using `git switch -c sandwich_test`
199+
- Add a new file `test_sandwich.py` with the function:
140200

141201
```python
142-
def test_subtract():
143-
assert subtract(5, 3) == 2
202+
from sandwich import sandwich
203+
204+
def test_sandwich():
205+
assert sandwich() == "======\njam\n======"
206+
```
207+
208+
- Push this to your repo with `git push -u origin sandwich_test`, but don't
209+
raise a pull request just yet.
210+
- Imagine that somebody else modifies the `main` branch before your branch can
211+
be merged -- something that happens all too often in open source projets!
212+
Return to the `main` branch using `git switch main`, and modify
213+
`sandwich.py`:
214+
215+
```python
216+
def sandwich():
217+
bread = "======"
218+
filling = "cheese"
219+
return "\n".join([bread, filling, bread])
220+
```
221+
222+
- Push this change to `main` with `git push`.
223+
- Now raise a pull request on your respository to merge `sandwich_test` into
224+
`main`.
225+
226+
After raising the pull request, you should see that the GitHub Action runs twice:
227+
once for a push, which will pass, and once for a pull request, which will fail:
228+
229+
![Example of tests failing on pull requests.](fig/pull_request_test_failed.png){alt="Example of tests failing on pull requests."}
230+
231+
To fix this:
232+
233+
- Switch to the `sandwich_test` branch, and call `git merge main`.
234+
- Fix the test on the `sandwich_test` branch, ensure the tests pass locally,
235+
and commit/push the changes.
236+
237+
The tests should now pass for both `push` and `pull_request`.
238+
239+
240+
## Testing across multiple platforms
241+
242+
A very useful feature of GitHub Actions is the ability to test over a wider
243+
range of platforms than just your own machine:
244+
245+
- Operating systems
246+
- Python versions
247+
- Compiler versions (for those writing C/C++/Fortran/etc)
248+
249+
We can achieve this by setting `jobs.<job_id>.strategy.matrix` in our workflow:
250+
251+
```yaml
252+
jobs:
253+
build:
254+
strategy:
255+
matrix:
256+
python_version: ["3.12", "3.13", "3.14"]
257+
os: ["ubuntu-latest", "windows-latest"]
258+
runs-on: ${{ matrix.os }}
259+
steps:
260+
...
261+
```
262+
263+
Later in the file, the `setup-python` step should be changed to:
264+
265+
```yaml
266+
- name: Set up Python ${{ matrix.python_version }}
267+
uses: actions/setup-python@v6
268+
with:
269+
python-version: ${{ matrix.python_version }}
144270
```
145271

146-
- Commit the changes using `git add .` and `git commit -m "Add subtract function"`
147-
- Push the changes to GitHub using `git push origin subtract`
272+
By default, all combinations in the matrix will be run in separate jobs. The
273+
syntax `${{ matrix.x }}` inserts the text from the `x` list for the given matrix job.
274+
275+
- Switch to a new branch `matrix_test` using `git switch -c matrix_test`
276+
- Make the changes above to `.github/workflows/tests.yaml`
277+
- Commit, push, and raise a pull request to `main`.
278+
279+
You should now see that _12_ separate jobs run!
280+
281+
![Completed matrix jobs.](fig/matrix_tests.png){alt="Completed matrix tests."}
282+
283+
This ensures that code that runs on your machine should, in theory, run on many
284+
other peoples' machines too. However, it's best to restrict the matrix to the
285+
minimum number of necessary platforms to ensure you don't waste resources. You
286+
can do so with a list of exclusions:
287+
288+
```yaml
289+
strategy:
290+
matrix:
291+
python_version: ["3.12", "3.13", "3.14"]
292+
os: ["ubuntu-latest", "windows-latest"]
293+
# Only run windows on latest Python version
294+
exclude:
295+
- os: "windows-latest"
296+
python_version: "3.12"
297+
- os: "windows-latest"
298+
python_version: "3.13"
299+
````
148300
149-
- Now go to your GitHub repository and create a new Pull Request to merge the `subtract` branch into `main`
150301
151-
You should see that the GitHub Action runs the tests and fails because the test for the `subtract` function is failing.
302+
## Other tips
303+
304+
You may have wondered why there is a trailing colon when we specify `push:` or `pull_request:`.
305+
The reason is that we can optionally set additional conditions on when they'll run. For example:
306+
307+
```yaml
308+
on:
309+
push:
310+
# Only check when Python files are changed.
311+
# Don't need to check when the README is updated!
312+
paths:
313+
- '**.py'
314+
- 'pyproject.toml'
315+
pull_request:
316+
paths:
317+
- '**.py'
318+
- 'pyproject.toml'
319+
# Only check when somebody raises a pull_request to main.
320+
branches: [main]
321+
# This allows you to run the tests manually if you choose
322+
workflow_dispatch:
323+
```
324+
325+
Doing this can prevent pointless CI jobs from running and save resources.
152326

153-
- Let's now fix the test and commit the changes: `git add .` and `git commit -m "Fix subtract function"`
154-
- Push the changes to GitHub using `git push origin subtract` again
155-
- Go back to the Pull Request on GitHub and you should see that the tests are now passing and you can merge the code into the main branch.
327+
## Keypoints
156328

157329
So now, when you or your team want to make a feature or just update the code, the workflow is as follows:
158330

@@ -171,7 +343,7 @@ This will greatly improve the quality of your code and make it easier to collabo
171343
- Continuous Integration (CI) is the practice of automating the merging of code changes into a project.
172344
- GitHub Actions is a feature of GitHub that allows you to automate the testing of your code.
173345
- GitHub Actions are defined in `yaml` files and are stored in the `.github/workflows` directory in your repository.
174-
- You can use GitHub Actions to only allow code to be merged into the main branch if the tests pass.
346+
- You can use GitHub Actions to ensure your tests pass before merging new code into your `main` branch.
175347

176348
::::::::::::::::::::::::::::::::::::::::::::::::
177349

episodes/fig/github_action.png

98 KB
Loading
8.88 KB
Loading

episodes/fig/github_repo_view.png

34.3 KB
Loading

episodes/fig/matrix_tests.png

101 KB
Loading
49.2 KB
Loading

0 commit comments

Comments
 (0)