Skip to content

Commit 0301fb3

Browse files
eduralphclaude
andcommitted
Add CI/CD pipeline with container-based testing
Introduce GitHub Actions CI workflow running inside a shared Docker image on ghcr.io, with a native Windows runner for cross-platform unit-test coverage. Add a shared pytest harness with Gramps-backed fixtures and repo-wide integration tests that verify every addon registers, loads, and exposes valid plugin metadata. Fix lint and structural issues across 29 addons so the pipeline goes green. CI infrastructure ----------------- - .github/docker/gramps-ci/Dockerfile — Python 3.12 + Gramps 6.0 (pip) + PyGObject + GTK typelibs + xvfb/xauth + ruff, pytest, 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/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 doesn't hang), build (make.py gramps60 build all). - .github/environment.yml — hybrid conda+pip env for Windows. Gramps isn't on conda-forge, so pygobject/gtk3 come from conda and gramps/orjson/pytest/dbf come from pip. Shared test harness ------------------- - tests/conftest.py — fixtures: gramps_user, gramps_plugin_manager, gramps_plugin_registry, gramps_db (fresh in-memory per test), gramps_db_session (shared). - tests/test_plugin_registration.py — registers every addon, subprocess-isolates module loading (crash-safe), verifies gramps_target_version=6.0 and valid id/name/version, smoke-tests import/export entry functions. - conftest.py (repo root) — registers the `gui` marker and auto-skips @pytest.mark.gui tests when GTK is unimportable. - pytest.ini — declares the `gui` marker. Lint fixes (79 → 0 ruff errors across 29 files) ---------------------------------------------- - Missing imports added: WindowActiveError, ErrorDialog, DbTxn, EditDate, ReportError, EventType, Surname, display_help, reduce, time, sys, etc. - 3× `os.name is 'nt'` → `==`; tuple-in-if bug in LifeLineChartView. - Py2 leftovers: `except E(msg)` → `except E as msg`, `unicode()` → `str()`, dropped basestring/reload branches. - Real bugs: missing `value` param in libaccess lambda, wrong var in JSONImport LOG.warn branch, `displayer.display` → `name_displayer.display` in QuiltView, missing paren in QueryQuickview, stray `parent=self.uistate.window` at module level in lxmlGramplet. - Dead-code cleanup: AttachSourceTool, SourceIndex/index.py, DynamicWeb/run_dynamicweb.py. - Renamed SurnameMappingGramplet.grp.py → .gpr.py (typo — Gramps never loaded this file). Addon structure --------------- - Added po/template.pot stubs for AnniversariesGramplet, ArchiveAssist, GrampsChat, GrampyScript (had no po/ dir at all). TMGimporter tests ----------------- - test_libtmg.py: 58 pure-logic tests (strip codes, date parsing, repo type / URL inference). Runs on Linux + Windows. - test_integration.py: 117 DB-backed tests (E2E pipeline + 12 function-level classes). Linux-only — in-memory Gramps SQLite hangs on Windows under pip-Gramps + conda-forge GTK, so the Windows job skips it via --ignore-glob='**/test_integration*.py'. Misc ---- - .gitignore: debug.log (was accidentally committed once); CLAUDE.md (fork-local AI guidance file). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b4457a9 commit 0301fb3

48 files changed

Lines changed: 2178 additions & 1257 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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, pytest, dbf for lint/test tooling
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+
pytest \
46+
ruff \
47+
dbf
48+
49+
RUN apt-get purge -y gcc python3-dev pkg-config && apt-get autoremove -y
50+
51+
RUN python -c "from gramps.gen.const import VERSION; print('Gramps', VERSION)" \
52+
&& python -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('GTK OK')"
53+
54+
WORKDIR /workspace

.github/environment.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
- pytest
13+
- dbf

.github/workflows/ci.yml

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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+
test_dirs=""
99+
for d in */tests/; do
100+
if ls "$d"/test_*.py 1>/dev/null 2>&1; then
101+
test_dirs="$test_dirs $d"
102+
fi
103+
done
104+
if [ -n "$test_dirs" ]; then
105+
echo "Running unit tests in: $test_dirs"
106+
python3 -m pytest $test_dirs -v --tb=short \
107+
--ignore-glob='**/test_integration*.py' \
108+
--ignore=Sqlite/tests/test_sqlite.py \
109+
-m 'not gui'
110+
else
111+
echo "No per-addon unit test files found"
112+
fi
113+
114+
# -----------------------------------------------------------------
115+
# Unit tests — Windows (conda-forge: bundles PyGObject + GTK + Gramps)
116+
# -----------------------------------------------------------------
117+
unit-test-windows:
118+
name: Unit Tests (Windows)
119+
runs-on: windows-latest
120+
defaults:
121+
run:
122+
shell: bash -el {0}
123+
steps:
124+
- uses: actions/checkout@v4
125+
126+
- name: Set up Miniforge
127+
uses: conda-incubator/setup-miniconda@v3
128+
with:
129+
miniforge-version: latest
130+
activate-environment: addons-ci
131+
environment-file: .github/environment.yml
132+
use-mamba: true
133+
134+
- name: Verify environment
135+
run: |
136+
mamba info
137+
mamba list | head -30
138+
python -c "import pytest, gramps, gi; print('deps OK')"
139+
140+
- name: Run per-addon unit tests
141+
env:
142+
PYTHONPATH: .
143+
run: |
144+
test_dirs=""
145+
for d in */tests/; do
146+
if ls "$d"/test_*.py 1>/dev/null 2>&1; then
147+
test_dirs="$test_dirs $d"
148+
fi
149+
done
150+
if [ -n "$test_dirs" ]; then
151+
echo "Running unit tests in: $test_dirs"
152+
python -m pytest $test_dirs -v --tb=short \
153+
--ignore-glob='**/test_integration*.py' \
154+
--ignore=Sqlite/tests/test_sqlite.py \
155+
-m 'not gui'
156+
else
157+
echo "No per-addon unit test files found"
158+
fi
159+
160+
# -----------------------------------------------------------------
161+
# Integration tests — Gramps (ci container, xvfb available)
162+
# -----------------------------------------------------------------
163+
integration-test:
164+
name: Integration Tests (Gramps)
165+
runs-on: ubuntu-latest
166+
needs: [unit-test-linux]
167+
container:
168+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
169+
options: --init
170+
steps:
171+
- uses: actions/checkout@v4
172+
173+
- name: Run plugin registration tests
174+
env:
175+
PYTHONPATH: .
176+
run: python3 -m pytest tests/ -v --tb=short
177+
178+
- name: Run per-addon integration tests
179+
env:
180+
PYTHONPATH: .
181+
run: |
182+
integration_tests=""
183+
for f in */tests/test_integration*.py */test_integration*.py; do
184+
if [ -f "$f" ]; then
185+
integration_tests="$integration_tests $f"
186+
fi
187+
done
188+
if [ -n "$integration_tests" ]; then
189+
echo "Running per-addon integration tests: $integration_tests"
190+
python3 -m pytest $integration_tests -v --tb=short
191+
else
192+
echo "No per-addon integration test files found"
193+
fi
194+
195+
# -----------------------------------------------------------------
196+
# Build (ci container)
197+
# -----------------------------------------------------------------
198+
build:
199+
name: Build
200+
runs-on: ubuntu-latest
201+
container:
202+
image: ghcr.io/${{ github.repository }}/gramps-ci:gramps60
203+
steps:
204+
- uses: actions/checkout@v4
205+
206+
- name: Determine GRAMPSPATH
207+
id: gramps-path
208+
run: |
209+
GPATH=$(python3 -c "import gramps, os; print(os.path.dirname(os.path.dirname(gramps.__file__)))")
210+
echo "path=$GPATH" >> "$GITHUB_OUTPUT"
211+
212+
- name: Build all addons
213+
env:
214+
GRAMPSPATH: ${{ steps.gramps-path.outputs.path }}
215+
run: |
216+
mkdir -p ../download
217+
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

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22
*.py[cod]
33
__pycache__
44

5+
# Gramps runtime logs
6+
debug.log
7+
58
# Editing
69
*~
710
tags
811
.idea
912

13+
# AI assistants (fork-local guidance files)
14+
CLAUDE.md
15+
1016
# Translations
1117
*.mo
1218

AnniversariesGramplet/AnniversariesGramplet.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from gramps.gen.utils.db import get_participant_from_event
2727
from gramps.gen.datehandler import get_date
2828
from gramps.gen.config import config
29+
from gramps.gen.errors import WindowActiveError
2930
from gramps.gui.editors import EditEvent
3031

3132
from gramps.gen.const import GRAMPS_LOCALE as glocale
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SOME DESCRIPTIVE TITLE.
2+
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3+
# This file is distributed under the same license as the PACKAGE package.
4+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5+
#
6+
#, fuzzy
7+
msgid ""
8+
msgstr ""
9+
"Project-Id-Version: PACKAGE VERSION\n"
10+
"Report-Msgid-Bugs-To: \n"
11+
"POT-Creation-Date: 2026-04-17 00:00+0000\n"
12+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14+
"Language-Team: LANGUAGE <LL@li.org>\n"
15+
"Language: \n"
16+
"MIME-Version: 1.0\n"
17+
"Content-Type: text/plain; charset=UTF-8\n"
18+
"Content-Transfer-Encoding: 8bit\n"

0 commit comments

Comments
 (0)