Files
bugsink/.github/workflows/ci.yml
Klaas van Schelven b81f754b8c Support Python 3.14
make it so by [a] saying it is so and [b] testing it in the CI/CD
2025-11-09 20:12:55 +01:00

243 lines
8.8 KiB
YAML

name: Continuous Integration
# we use the fact that MySQL and Postgres are part of the ubuntu image, which saves us from
# "service container" complexities (but gives no choices in the version); this is also where
# the confured username/passwords for mysql come from
# approach inspired by https://blog.healthchecks.io/2020/11/using-github-actions-to-run-django-tests/
on:
push:
# hardcoded list; GitHub does not support wildcards in branch names AFAICT
branches: [ "main", "1.4.x", "1.5.x", "1.6.x", "1.7.x", "2.0.x", "2.1.x", "2.2.x", "2.3.x", "2.4.x", "2.5.x", "2.6.x", "2.7.x", "2.8.x"]
pull_request:
branches: [ "main" ]
workflow_dispatch: # Enables manual invocation via "Run workflow" button in the Actions UI
inputs:
target_branch:
description: 'Branch to run workflow on'
required: false
default: 'main'
env:
DJANGO_SETTINGS_MODULE: "bugsink.settings.development"
jobs:
flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Set up Python 3.11"
uses: actions/setup-python@v5
with:
python-version: "3.11" # below 3.12 to avoid false positives inside f-string; https://github.com/PyCQA/pycodestyle/issues/1260
- name: Install Flake8
run: |
python -m pip install --upgrade flake8
- name: Run Flake8
run: |
# We ignore 2 classes of whitespace errors (which are useful in the local context,
# but not worth breaking the build).
# https://github.com/PyCQA/flake8/issues/515 shows a dead end of doing this "properly"
# so we just specify it on the command line
flake8 --extend-ignore=E127,E741,E501,E731 `git ls-files | grep py$`
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install Bandit and Plugins
run: |
pip install bandit spoils
- name: Run Bandit and format results
shell: bash
run: |
# set +e disables "exit on any non-zero command" behavior
# set +o pipefail disables GH's default "fail the whole pipeline if any stage fails"
set +e +o pipefail
# Note: .py files only; at the time of writing I checked the conf_templates/*.template
# also; but they had 2 False positives only (SECRET_KEY lives there by design) and I
# don't want to pollute templates that other people deal with with "nosec".
bandit_json_output=$( \
git ls-files \
| grep '\.py$' \
| xargs bandit -q -f json --ini .bandit \
)
bandit_exit_code=$?
echo "$bandit_json_output" \
| jq -r '
.results[]
| [ .filename
, .line_number
, .test_id
, .issue_confidence
, .issue_severity
, .test_name
]
| @tsv
' \
| column -t
if [[ $bandit_exit_code -ne 0 ]]; then
echo "Bandit found issues (exit code $bandit_exit_code)"
exit $bandit_exit_code
fi
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
db: [sqlite, mysql, postgres]
include:
- db: mysql
db_user: root
db_password: root
- db: postgres
db_user: bugsink
db_password: bugsink
env:
DB: ${{ matrix.db }}
DB_USER: ${{ matrix.db_user }}
DB_PASSWORD: ${{ matrix.db_password }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Start MySQL If Needed
if: matrix.db == 'mysql'
run: sudo systemctl start mysql.service
- name: Start PostgreSQL
if: matrix.db == 'postgres'
run: |
sudo systemctl start postgresql.service
sudo -u postgres psql -c "CREATE ROLE bugsink PASSWORD 'bugsink' NOSUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;" postgres
- name: Install build
run: |
python -m pip install --upgrade pip
pip install build
- name: Build wheel
run: |
python -m build --wheel
- name: Install from wheel
run: |
python -m pip install dist/*.whl
- name: Install development dependencies
run: |
pip install -r requirements.development.txt
pip install mysqlclient psycopg2
- name: Check out event-samples
uses: actions/checkout@master
with:
repository: bugsink/event-samples
path: "event-samples"
- name: Create separate dir to avoid accidentally using non-packaged code
run: |
mkdir separate_dir
- name: Run Makemigrations --check
working-directory: separate_dir
run: |
bugsink-manage makemigrations --check
- name: Check bugsink-manage in settingless mode
working-directory: separate_dir
shell: bash
run: |
# Run the console entry in the most trivial way (--help), but with DJANGO_SETTINGS_MODULE truly unset
env -u DJANGO_SETTINGS_MODULE bugsink-manage --help >/dev/null
- name: Run Tests
working-directory: separate_dir
env:
SAMPLES_DIR: "${{ github.workspace }}/event-samples"
PYTHONWARNINGS: all
run: |
# because we're outside the project directory, the test discovery won't find our packages. We simply enumerate
# them using some shell-magic. Note that the only non-apps that we still care about are 'bugsink' (project, not
# app), and sentry_sdk_extensions, which we mention separately
bugsink-manage test `bugsink-manage shell -v0 -c 'from django.conf import settings; print(" ".join(settings.BUGSINK_APPS))'` bugsink sentry_sdk_extensions -v2
# bugsink-manage test ${GITHUB_WORKSPACE} -v2 # fails with the following, which I don't understand:
# ImportError: 'tests' module incorrectly imported from '/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/alerts'. Expected '/home/runner/work/bugsink-private/bugsink-private/alerts'. Is this module globally installed?
docker:
runs-on: ubuntu-latest
# Build the Docker image, start it, wait for readiness, log in at /accounts/login/,
# and verify that / redirects to /teams/.
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t bugsink-ci .
- name: Run container (admin:admin)
run: |
docker rm -f bugsink-ci || true
docker run -d --name bugsink-ci \
-e SECRET_KEY=UtdHATKzQKLkz3izFFpxCKamhEdspeHmCQFvLx1DAwskqEuyKL \
-e CREATE_SUPERUSER=admin:admin \
-e PORT=8000 \
-p 8000:8000 \
bugsink-ci
- name: Wait for health
run: |
for i in {1..20}; do
if curl -sf http://localhost:8000/health/ready >/dev/null; then
echo "Service is ready"
exit 0
fi
sleep 1
done
echo "Service did not become healthy in time"
docker logs bugsink-ci
exit 1
- name: Login via /accounts/login/ and expect redirect from /
shell: bash
run: |
rm -f cookies.txt headers_root.txt
# 1) Get the login page and receive csrftoken
curl -sS -c cookies.txt http://localhost:8000/accounts/login/ >/dev/null
CSRF=$(awk '$6=="csrftoken"{print $7}' cookies.txt | tail -n1)
if [ -z "$CSRF" ]; then
echo "No csrftoken from /accounts/login/"
exit 1
fi
# 2) POST credentials (plain Django fields)
CODE=$(curl -sS -b cookies.txt -c cookies.txt \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Referer: http://localhost:8000/accounts/login/" \
-d "username=admin&password=admin&csrfmiddlewaretoken=${CSRF}&next=/" \
-o /dev/null -w "%{http_code}" \
http://localhost:8000/accounts/login/)
if [ "$CODE" != "302" ]; then
echo "Login failed (status: $CODE)"
exit 1
fi
# 3) HEAD / and check redirect to /teams/
curl -sS -b cookies.txt -I http://localhost:8000/ > headers_root.txt
if ! grep -qi '^Location: /teams/' headers_root.txt; then
echo "Expected redirect to /teams/ not found"
echo "--- / (status + Location):"
sed -n '1p;/^Location:/Ip' headers_root.txt
echo "--- cookies (names):"
awk '{print $6}' cookies.txt | sort -u || true
exit 1
fi
echo "Login OK and / redirects to /teams/"