-
Notifications
You must be signed in to change notification settings - Fork 0
334 lines (316 loc) · 13.1 KB
/
CI.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
###############################################################################
# GitHub Actions vs. Branch Protections. Ugh.
#
# Being a complete novice to GitHub Actions, the documentation at GitHub
# heavily implies that the best way to limit push and pull_request events is to
# use paths and paths-ignore:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore
# Also, many of the real world examples I looked at use this approach.
#
# I had set up my two workflows, Code-CI.yml and Docs-CI.yml to ignore each
# other's directories and life was good as everything linted, build, tested,
# and deployed perfectly. The 99.82% code coverage was glorious, too!
#
# Then I decided to look at the cool Branch Protections..... RECORD SCRAAAATCH!
# It appears that the Actions team and the Branch Protections team have never
# spoken to one another, even at a party. 😹😹 If I set Branch Protections to
# require "Type & Lint Checks" and all of the "Tests & Coverage" matrix,
# any documentation PRs would never pass as doc jobs never would run those
# code jobs.
#
# Folks have been begging for a fix to this mess for over 5 years:
# https://github.com/orgs/community/discussions/26251
# https://github.com/orgs/community/discussions/13690
#
# "The Handling skipped but required checks" documentation says to use a
# merge_group but merge queues require an organization so are not available to
# little old me.
#
# https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks
# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#merge_group
# https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request-with-a-merge-queue
#
# Fortunately, there's the community dorny/paths-filter@v3 action.
# https://github.com/dorny/paths-filter
#
# This is my attempt to let me have the best of both worlds. I hope it works
# when I set the Branch Protection to require every job to pass, except the
# fight-github-job 🤣🤪. The docs say that if a job is skipped, its counted
# as passed.
###############################################################################
# Many thanks to https://rhysd.github.io/actionlint/ for saving my bacon!
name: "Code/Docs CI"
on:
# Runs on both pushes and PRs to main.
push:
branches: ["main"]
pull_request:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab, which is
# kind of important for developing this action as well as testing on any
# branch.
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment
concurrency:
group: "CI"
cancel-in-progress: true
# The work done in this action.
jobs:
#############################################################################
# The most important job in this file.
#############################################################################
fight-github-job:
name: "Detect File Changes"
runs-on: ubuntu-latest
# The permissions necessary for dorny/paths-filter@v3 on pull requests.
permissions:
pull-requests: read
# The outputs for this job.
outputs:
# The first two outputs are what are used in subsequent jobs to determine
# if we are looking at code or docs changes.
code: ${{steps.filter.outputs.code}}
docs: ${{steps.filter.outputs.docs}}
# These two here are for debugging purposes. After much trial and error,
# the ${FILTER_NAME}_files discussed in the dorny/paths-filter
# documentation only works on steps. By doing this one can get the files
# out of the job.
code-files: ${{steps.filter.outputs.code_files}}
docs-files: ${{steps.filter.outputs.docs_files}}
steps:
# Check out the code as that's needed for a push trigger.
- name: "Checkout Code"
uses: actions/checkout@v4
# Now do the filtering so we can appropriately decide which jobs need to
# be run.
- name: "Filter Files"
uses: dorny/paths-filter@v3
id: filter
with:
# For debugging, I'm setting the outputs to build a list of files so
# I can print them out in the jobs below.
list-files: shell
# For docs, I only care about all changes in the docs directory.
# For code, I care only about the .toml file and the two code
# directories, src and tests.
filters: |
docs:
- added|modified|deleted: 'docs/**'
code:
- modified:'pyproject.toml'
- added|modified|deleted: 'src/**'
- added|modified|deleted: 'tests/**'
- name: "Changed Code Files"
run: |
echo Code files: ${{steps.filter.outputs.code_files}}
- name: "Changed Docs Files"
run: |
echo Docs files: ${{steps.filter.outputs.docs_files}}
#############################################################################
# Code Jobs
#############################################################################
lint-types-job:
needs: fight-github-job
name: "Type & Lint Checks"
# Only run if we have changed code files.
if: ${{needs.fight-github-job.outputs.code == 'true'}}
runs-on: ubuntu-latest
steps:
# You have to love copying and pasting the same 11 lines into each job.
# I tried to make these a reusable job, but GitHub Actions wants to
# force you to ARY: Always Repeat Yourself. 😹😹
- name: "Checkout Code"
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: 'pip'
- name: "Install Dependencies"
run: |
python -m pip install --upgrade pip
python -m pip install .[dev]
# The unique part starts here.
- name: "Check Types"
run: mypy --config-file pyproject.toml src/ tests/
- name: Ruff Lint
run: ruff check --config ./pyproject.toml src/ tests/
- name: Pylint Lint
run: pylint --rcfile pyproject.toml src/ tests/
test-cov-job:
needs: [fight-github-job, lint-types-job]
name: "Tests & Coverage"
# Only run on changed code files.
if: ${{needs.fight-github-job.outputs.code == 'true'}}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.12"]
steps:
# Repeat ourselves again.
- name: "Checkout Code"
uses: actions/checkout@v4
- name: "Set up Python ${{ matrix.python-version }}"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: "Install Dependencies"
run: |
python -m pip install --upgrade pip
python -m pip install .[dev]
# The unique part of this job.
- name: "Run Tests and Coverage"
# Generate the coverage data for this operating system.
# The default name for the file is ".coverage", which is the same for
# all operating systems and makes combining them later a little hard.
# This uses the COVERAGE_FILE environment variable to give each their
# own name.
env:
COVERAGE_FILE: ".coverage.${{ matrix.os }}"
run: |
coverage run -m pytest --maxfail=1 -console_output_style=classic --junit-xml=.test-results.xml
- name: "Count Unit Tests"
# Count the number of unit tests for the badge.
if: runner.os == 'Linux'
run: |
export TOTAL_UNIT_TESTS=$(python ./tools/num_pytest_tests.py .test-results.xml)
echo "total_unit_tests=$TOTAL_UNIT_TESTS" >> $GITHUB_ENV
echo "## Total Unit Tests: :trophy: ${TOTAL_UNIT_TESTS} :1st_place_medal:" >> $GITHUB_STEP_SUMMARY
- name: "Make Unit Tests Badge"
# The unit test badge is only updated on tbp and for the main branch.
if: runner.os == 'Linux' && (github.ref == 'refs/heads/main')
# https://gist.github.com/John-Robbins/bd5e145f62ac1cf199a458977b8e1f16
uses: schneegans/dynamic-badges-action@v1.7.0
with:
# GIST_BADGES_SECRET is a GitHub personal access token with scope "gist".
auth: ${{ secrets.GIST_BADGES_SECRET }}
gistID: bd5e145f62ac1cf199a458977b8e1f16
filename: unittestsbadge.json
label: Unit Tests
message: ${{ env.total_unit_tests }}
minColorRange: 200
maxColorRange: 290
valColorRange: ${{ env.total_unit_tests }}
style: "flat-square"
- name: "Upload Coverage Data"
# Upload the coverage data for the coverage-job
uses: actions/upload-artifact@v4
with:
name: covdata-${{ matrix.os }}
path: .coverage*
# Loved the breaking change on 2024/09/02 without a major update.
# https://github.com/actions/upload-artifact/issues/602
# SemVer be damned!
include-hidden-files: true
cov-report-job:
needs: [fight-github-job, test-cov-job]
name: "Coverage Report"
# Only run on changed code files.
if: ${{needs.fight-github-job.outputs.code == 'true'}}
runs-on: ubuntu-latest
steps:
# Repeat ourselves again.
- name: "Checkout Code"
uses: actions/checkout@v4
- name: "Set up Python 3.12"
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: 'pip'
- name: "Install Dependencies"
run: |
python -m pip install --upgrade pip
python -m pip install .[dev]
# The unique part of this job.
- name: "Download Coverage Data"
# Gather all those operating system coverage files.
uses: actions/download-artifact@v4
with:
pattern: covdata-*
merge-multiple: true
- name: "Combine and Report"
# Get the code coverage data.
run: |
coverage combine
coverage report --precision=2 --show-missing --sort=Cover --skip-covered
coverage json --fail-under=98
export TOTAL_COVERAGE=$(python -c "import json;print(round(float(json.load(open('coverage.json'))['totals']['percent_covered']),2))")
echo "total_coverage=$TOTAL_COVERAGE" >> $GITHUB_ENV
echo "## Total coverage: :fire: ${TOTAL_COVERAGE}% :fireworks:" >> $GITHUB_STEP_SUMMARY
- name: "Make Coverage Badge"
# Code coverage is only updated on main branch.
if: (github.ref == 'refs/heads/main')
# https://gist.github.com/John-Robbins/bd5e145f62ac1cf199a458977b8e1f16
uses: schneegans/dynamic-badges-action@v1.7.0
with:
# GIST_BADGES_SECRET is a GitHub personal access token with scope "gist".
auth: ${{ secrets.GIST_BADGES_SECRET }}
gistID: bd5e145f62ac1cf199a458977b8e1f16
filename: covbadge.json
label: Coverage
message: ${{ env.total_coverage }}%
minColorRange: 50
maxColorRange: 90
valColorRange: ${{ env.total_coverage }}
style: "flat-square"
#############################################################################
# Documentation Jobs
#############################################################################
# Based off the excellent work at https://github.com/just-the-docs/just-the-docs-template.
# Thanks to all!
build-website-job:
needs: fight-github-job
name: "Build Web Site"
# Are there any document files that have changed?
if: ${{needs.fight-github-job.outputs.docs == 'true'}}
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
# Not needed with a .ruby-version file
ruby-version: '3.3'
# runs 'bundle install' and caches installed gems automatically
bundler-cache: true
# Increment this number if you need to re-download cached gems
cache-version: 0
working-directory: '${{ github.workspace }}/docs'
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
# Outputs to the './_site' directory by default
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Upload artifact
# Automatically uploads an artifact from the '.docs/_site' directory by
# default. Making sure include-hidden-files is true as, well, see above.
uses: actions/upload-pages-artifact@v3
with:
path: "docs/_site"
deploy-website-job:
needs: [fight-github-job, build-website-job]
name: "Deploy Web Site"
# If there are changed documents and this is the main branch, we'll deploy.
if: (needs.fight-github-job.outputs.docs == 'true') && (github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4