Skip to content

Commit b677454

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. Scope note: this PR introduces CI scaffolding only. Existing lint/structure issues across the addon set (~79 ruff errors, four addons missing po/template.pot, etc.) will be addressed through separate PRs before the corresponding CI gates are activated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8c3880b commit b677454

7 files changed

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