Skip to content

Commit 774a9ac

Browse files
eduralphclaude
andcommitted
Add CI/CD pipeline with container-based testing (issue 9393)
Introduce GitHub Actions CI inside a shared Docker image on ghcr.io, plus a native Windows runner for cross-platform unit-test coverage, and a shared unittest harness with Gramps-backed fixtures that verifies every addon registers, loads, and exposes valid plugin metadata. CI infrastructure ----------------- - .github/docker/gramps-ci/Dockerfile — Python 3.12 + Gramps 6.0 (pip) + PyGObject + GTK typelibs + xvfb/xauth + ruff, dbf, intltool, gettext, git. GTK lives in the base so addon modules that do `from gi.repository import Gtk` at load time are importable; xvfb and xauth are bundled for tests that actually render. - .github/workflows/docker-build.yml — rebuilds the image on .github/docker/** changes or via workflow_dispatch. - .github/workflows/ci.yml — seven jobs: lint (ruff E9/F63/F7/F82 + trailing whitespace), addon-structure (every addon has po/template.pot), compile-check (py_compile on every .py), unit-test-linux (container), unit-test-windows (native, conda+pip), integration-test (container with --init so xvfb-run does not hang), build (make.py gramps60 build all). - .github/environment.yml — hybrid conda+pip env for Windows. Gramps is not on conda-forge, so pygobject/gtk3 come from conda and gramps/orjson/dbf come from pip. Shared test harness ------------------- - tests/__init__.py — GPL header. - tests/gramps_test_env.py — sys.path / GRAMPS_RESOURCES bootstrap and two unittest base classes: GrampsTestCase (session-cached plugin manager + registry via setUpClass) and GrampsDbTestCase (same plus a fresh in-memory SQLite DB per test). - tests/test_plugin_registration.py — four unittest.TestCase classes covering plugin registration, subprocess-isolated module loading (crash-safe), required metadata (gramps_target_version=6.0, valid id/name/version), and import/export entry-function smoke tests. Gate policy ----------- All seven jobs run on every push and PR. Four are marked continue-on-error: true so they surface issues without blocking merges while the existing tree is cleaned up: - lint (~79 pre-existing ruff E9/F63/F7/F82 errors) - addon-structure (4 addons missing po/template.pot) - unit-test-linux (some addon test modules fail to import today) - unit-test-windows (same) compile-check, integration-test, and build are blocking from day one. Each non-blocking gate will be flipped to blocking in the same follow-up PR that fixes its underlying issues, so the tightening is incremental and visible in history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8c3880b commit 774a9ac

7 files changed

Lines changed: 830 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# .github/docker/gramps-ci/Dockerfile
2+
#
3+
# Unified Gramps 6.0 CI image. Includes everything jobs need:
4+
# - Python + pip-installed Gramps, PyGObject, pycairo
5+
# - GTK typelibs (so addon modules that `from gi.repository import Gtk`
6+
# at module load time are importable — widgets still need xvfb to render)
7+
# - intltool/gettext/git for make.py builds
8+
# - ruff, dbf for lint/test tooling (tests use stdlib unittest per AGENTS.md)
9+
# - xvfb + xauth for tests that actually render (wrap with `xvfb-run`)
10+
#
11+
# No display server runs by default — the image is headless unless a command
12+
# explicitly invokes `xvfb-run`. When running with docker locally, pass
13+
# `--init` (or use a container runtime that injects tini) because xvfb-run
14+
# hangs if it inherits PID 1.
15+
#
16+
ARG PYTHON_VERSION=3.12
17+
FROM python:${PYTHON_VERSION}-slim
18+
19+
LABEL org.opencontainers.image.source="https://github.com/gramps-project/addons-source"
20+
LABEL org.opencontainers.image.description="Unified Gramps 6.0 CI image (Python, Gramps, GTK typelibs, xvfb)"
21+
22+
RUN apt-get update && apt-get install -y --no-install-recommends \
23+
libgirepository-2.0-dev \
24+
gir1.2-glib-2.0 \
25+
gir1.2-gtk-3.0 \
26+
gir1.2-pango-1.0 \
27+
gir1.2-gdkpixbuf-2.0 \
28+
gir1.2-atk-1.0 \
29+
gcc \
30+
pkg-config \
31+
python3-dev \
32+
libcairo2-dev \
33+
intltool \
34+
gettext \
35+
git \
36+
xvfb \
37+
xauth \
38+
&& rm -rf /var/lib/apt/lists/*
39+
40+
RUN pip install --no-cache-dir \
41+
PyGObject \
42+
pycairo \
43+
"gramps>=6.0,<6.1" \
44+
orjson \
45+
ruff \
46+
dbf
47+
48+
RUN apt-get purge -y gcc python3-dev pkg-config && apt-get autoremove -y
49+
50+
RUN python -c "from gramps.gen.const import VERSION; print('Gramps', VERSION)" \
51+
&& python -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('GTK OK')"
52+
53+
WORKDIR /workspace

.github/environment.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: addons-ci
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- python=3.12
6+
- pygobject
7+
- gtk3
8+
- pip
9+
- pip:
10+
- "gramps>=6.0,<6.1"
11+
- orjson
12+
- dbf

.github/workflows/ci.yml

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [maintenance/gramps60]
6+
pull_request:
7+
branches: [maintenance/gramps60]
8+
9+
env:
10+
CI_IMAGE: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
11+
12+
jobs:
13+
# -----------------------------------------------------------------
14+
# Lint (ci container)
15+
# -----------------------------------------------------------------
16+
lint:
17+
name: Lint
18+
runs-on: ubuntu-latest
19+
# Non-blocking until the existing ruff E9/F63/F7/F82 errors across the
20+
# addon set are cleaned up in a follow-up PR. Flip this off in that PR.
21+
continue-on-error: true
22+
container:
23+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Run ruff (syntax and import errors only)
28+
run: ruff check --select=E9,F63,F7,F82 --no-fix --exclude='*.gpr.py' .
29+
30+
- name: Check trailing whitespace in Python files
31+
run: |
32+
if git --no-pager grep --color -n --full-name '[ \t]$' -- '*.py'; then
33+
echo "::error::Trailing whitespace found in Python files"
34+
exit 1
35+
fi
36+
37+
# -----------------------------------------------------------------
38+
# Addon structure (bare runner — just bash, no deps needed)
39+
# -----------------------------------------------------------------
40+
addon-structure:
41+
name: Addon Structure
42+
runs-on: ubuntu-latest
43+
# Non-blocking until the four addons missing po/template.pot are fixed
44+
# in a follow-up PR. Flip this off in that PR.
45+
continue-on-error: true
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Check all addons have po/template.pot
50+
run: |
51+
failed=0
52+
for gpr in */*.gpr.py; do
53+
addon_dir="$(dirname "$gpr")"
54+
if [ ! -d "$addon_dir/po" ]; then
55+
echo "::error::$addon_dir is missing po/ directory"
56+
failed=1
57+
elif [ ! -f "$addon_dir/po/template.pot" ]; then
58+
echo "::error::$addon_dir is missing po/template.pot"
59+
failed=1
60+
fi
61+
done
62+
if [ "$failed" -eq 0 ]; then
63+
echo "All addons have po/template.pot"
64+
fi
65+
exit $failed
66+
67+
# -----------------------------------------------------------------
68+
# Compile check (ci container)
69+
# -----------------------------------------------------------------
70+
compile-check:
71+
name: Compile Check
72+
runs-on: ubuntu-latest
73+
container:
74+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Compile all Python files (excluding .gpr.py)
79+
shell: bash
80+
run: |
81+
failed=0
82+
while IFS= read -r f; do
83+
if ! python3 -m py_compile "$f" 2>&1; then
84+
failed=1
85+
fi
86+
done < <(find . -name '*.py' ! -name '*.gpr.py' ! -path './.git/*' ! -path '*/__pycache__/*')
87+
exit $failed
88+
89+
# -----------------------------------------------------------------
90+
# Unit tests — Linux (ci container)
91+
# -----------------------------------------------------------------
92+
unit-test-linux:
93+
name: Unit Tests (Linux)
94+
runs-on: ubuntu-latest
95+
# Non-blocking until the currently-broken addon unit modules (import
96+
# failures, stale API usage) are sorted out in follow-up PRs.
97+
continue-on-error: true
98+
container:
99+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
100+
steps:
101+
- uses: actions/checkout@v4
102+
103+
- name: Run per-addon unit tests
104+
env:
105+
PYTHONPATH: .
106+
run: |
107+
modules=""
108+
for f in */tests/test_*.py; do
109+
[ -f "$f" ] || continue
110+
case "$(basename "$f")" in
111+
test_integration*) continue ;;
112+
esac
113+
case "$f" in
114+
Sqlite/tests/test_sqlite.py) continue ;;
115+
esac
116+
mod="${f%.py}"
117+
mod="${mod//\//.}"
118+
modules="$modules $mod"
119+
done
120+
if [ -n "$modules" ]; then
121+
echo "Running unit tests:$modules"
122+
python3 -m unittest -v $modules
123+
else
124+
echo "No per-addon unit test modules found"
125+
fi
126+
127+
# -----------------------------------------------------------------
128+
# Unit tests — Windows (conda-forge: bundles PyGObject + GTK + Gramps)
129+
# -----------------------------------------------------------------
130+
unit-test-windows:
131+
name: Unit Tests (Windows)
132+
runs-on: windows-latest
133+
# Non-blocking for the same reason as unit-test-linux.
134+
continue-on-error: true
135+
defaults:
136+
run:
137+
shell: bash -el {0}
138+
steps:
139+
- uses: actions/checkout@v4
140+
141+
- name: Set up Miniforge
142+
uses: conda-incubator/setup-miniconda@v3
143+
with:
144+
miniforge-version: latest
145+
activate-environment: addons-ci
146+
environment-file: .github/environment.yml
147+
use-mamba: true
148+
149+
- name: Verify environment
150+
run: |
151+
mamba info
152+
mamba list | head -30
153+
python -c "import gramps, gi; print('deps OK')"
154+
155+
- name: Run per-addon unit tests
156+
env:
157+
PYTHONPATH: .
158+
run: |
159+
modules=""
160+
for f in */tests/test_*.py; do
161+
[ -f "$f" ] || continue
162+
case "$(basename "$f")" in
163+
test_integration*) continue ;;
164+
esac
165+
case "$f" in
166+
Sqlite/tests/test_sqlite.py) continue ;;
167+
esac
168+
mod="${f%.py}"
169+
mod="${mod//\//.}"
170+
modules="$modules $mod"
171+
done
172+
if [ -n "$modules" ]; then
173+
echo "Running unit tests:$modules"
174+
python -m unittest -v $modules
175+
else
176+
echo "No per-addon unit test modules found"
177+
fi
178+
179+
# -----------------------------------------------------------------
180+
# Integration tests — Gramps (ci container, xvfb available)
181+
# -----------------------------------------------------------------
182+
integration-test:
183+
name: Integration Tests (Gramps)
184+
runs-on: ubuntu-latest
185+
needs: [unit-test-linux]
186+
container:
187+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
188+
options: --init
189+
steps:
190+
- uses: actions/checkout@v4
191+
192+
- name: Run plugin registration tests
193+
env:
194+
PYTHONPATH: .
195+
run: python3 -m unittest discover -s tests -p "test_*.py" -t . -v
196+
197+
- name: Run per-addon integration tests
198+
env:
199+
PYTHONPATH: .
200+
run: |
201+
modules=""
202+
for f in */tests/test_integration*.py; do
203+
[ -f "$f" ] || continue
204+
mod="${f%.py}"
205+
mod="${mod//\//.}"
206+
modules="$modules $mod"
207+
done
208+
if [ -n "$modules" ]; then
209+
echo "Running per-addon integration tests:$modules"
210+
python3 -m unittest -v $modules
211+
else
212+
echo "No per-addon integration test modules found"
213+
fi
214+
215+
# -----------------------------------------------------------------
216+
# Build (ci container)
217+
# -----------------------------------------------------------------
218+
build:
219+
name: Build
220+
runs-on: ubuntu-latest
221+
container:
222+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
223+
steps:
224+
- uses: actions/checkout@v4
225+
226+
- name: Determine GRAMPSPATH
227+
id: gramps-path
228+
run: |
229+
GPATH=$(python3 -c "import gramps, os; print(os.path.dirname(os.path.dirname(gramps.__file__)))")
230+
echo "path=$GPATH" >> "$GITHUB_OUTPUT"
231+
232+
- name: Build all addons
233+
env:
234+
GRAMPSPATH: ${{ steps.gramps-path.outputs.path }}
235+
run: |
236+
mkdir -p ../download
237+
python3 make.py gramps60 build all

.github/workflows/docker-build.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Build Docker Images
2+
3+
on:
4+
push:
5+
branches: [maintenance/gramps60]
6+
paths:
7+
- '.github/docker/**'
8+
workflow_dispatch:
9+
10+
env:
11+
REGISTRY: ghcr.io
12+
REPO: ${{ github.repository }}
13+
14+
permissions:
15+
contents: read
16+
packages: write
17+
18+
jobs:
19+
build-ci:
20+
name: Build gramps-ci
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Log in to GHCR
26+
uses: docker/login-action@v3
27+
with:
28+
registry: ghcr.io
29+
username: ${{ github.actor }}
30+
password: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Set up Docker Buildx
33+
uses: docker/setup-buildx-action@v3
34+
35+
- name: Docker metadata
36+
id: meta
37+
uses: docker/metadata-action@v5
38+
with:
39+
images: ${{ env.REGISTRY }}/${{ env.REPO }}/gramps-ci
40+
tags: |
41+
type=raw,value=gramps60
42+
type=sha,prefix=gramps60-
43+
44+
- name: Build and push gramps-ci
45+
uses: docker/build-push-action@v6
46+
with:
47+
context: .github/docker/gramps-ci
48+
push: true
49+
tags: ${{ steps.meta.outputs.tags }}
50+
labels: ${{ steps.meta.outputs.labels }}
51+
cache-from: type=gha
52+
cache-to: type=gha,mode=max

tests/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2026 Eduard Ralph
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19+
#
20+
21+
"""Package marker for the repo-wide Gramps addon test suite."""

0 commit comments

Comments
 (0)