Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
774a9ac
Add CI/CD pipeline with container-based testing (issue 9393)
eduralph Apr 19, 2026
c6aa10e
CI: auto-derive addon pip deps from requires_mod in .gpr.py
eduralph Apr 20, 2026
8d2654a
CI: remove dbf from image/env — installed via auto-derive now
eduralph Apr 20, 2026
28febdc
CI: add shell: bash to unit-test-linux + integration-test steps
eduralph Apr 20, 2026
715e71d
CI: split OS-specific addon tests via filename convention
eduralph Apr 20, 2026
dd0fd38
CI image: add gir1.2-gexiv2-0.10 for EditExifMetadata + PhotoTagging
eduralph May 11, 2026
205b21c
CI: lint trailing-whitespace check — switch BRE [ \t] to PCRE
eduralph May 11, 2026
f5907af
ci: skip addons whose .gpr.py declares include_in_listing=False
eduralph May 15, 2026
ff215ac
CI: validate requires_mod names against find_spec (Pillow/PIL trap)
eduralph May 15, 2026
d265612
CI: make lint job blocking — ruff backlog cleared on maintenance/gram…
eduralph May 19, 2026
0dd3f1b
CI: make unit-test-linux and unit-test-windows blocking — backlog cle…
eduralph May 19, 2026
9a91d89
CI: derive image tag and make.py argument from the branch ref
eduralph May 18, 2026
abefdbe
docker-build: derive image tag and Gramps series from the branch ref
eduralph May 18, 2026
9927626
Dockerfile: parameterise Gramps series via GRAMPS_SERIES build arg
eduralph May 18, 2026
894e484
CI image: hybrid PyPI / git-clone install for unreleased maintenance …
eduralph May 18, 2026
73d75ed
docs: note the first-push race and unreleased-branch CI semantics
eduralph May 18, 2026
3b2a947
docs: add CI maintainer runbook (.github/CI-MAINTAINER.md)
eduralph May 18, 2026
ad2815c
tests: detect addons that import a sibling without depends_on
eduralph May 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions .github/CI-MAINTAINER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# CI Maintainer Notes

Operational steps that a `gramps-project/addons-source` maintainer
needs to handle once PR 820 (and the branch-neutral follow-up) lands.
The pipeline is otherwise self-driving — this document is only about
the rough edges.

For day-to-day addon-release maintenance see [MAINTAINERS.md](../MAINTAINERS.md);
for the contributor-facing summary of what a green CI check means on
an unreleased branch see [CONTRIBUTING.md](../CONTRIBUTING.md#work-towards-a-merge).

## Contents

1. [One-time setup when the PR first merges](#one-time-setup-when-the-pr-first-merges)
2. [Creating a new maintenance branch](#creating-a-new-maintenance-branch)
3. [When a Gramps minor release lands on PyPI](#when-a-gramps-minor-release-lands-on-pypi)
4. [Diagnostic log markers](#diagnostic-log-markers)
5. [Optional future-proofing knobs](#optional-future-proofing-knobs)

## One-time setup when the PR first merges

### 1. Make the `gramps-ci` GHCR package public

`docker-build.yml` pushes images to
`ghcr.io/gramps-project/addons-source/gramps-ci:<suffix>` using the
workflow's `GITHUB_TOKEN`. GHCR creates the package as **private** the
first time. Same-repo CI keeps working (token covers own packages),
but **fork PRs cannot pull the image** because their `GITHUB_TOKEN`
has no read access to private packages in `gramps-project`. Fork-PR
container jobs would fail at "Initialize containers" with an
authentication error.

Fix once, immediately after the first `Build Docker Images` run
finishes:

1. Go to <https://github.com/orgs/gramps-project/packages/container/addons-source%2Fgramps-ci/settings>
2. Under "Danger Zone" → "Change visibility" → set to **Public**

Every existing and future `gramps-ci:<suffix>` tag inherits public
visibility from this single setting.

### 2. Expect the first-push race on `maintenance/gramps60`

The first push event after merge fires both workflows in parallel:

- `Build Docker Images` builds and pushes `gramps-ci:gramps60`
(~5 min cold).
- `CI` runs `setup` (~2 s), then its container jobs try to pull the
image.

Because both start on the same push, the CI container jobs race the
image push and may fail at "Initialize containers" the first time.
This race only happens once per branch:

1. Wait for `Build Docker Images` to complete.
2. Open the failed CI run → click "Re-run failed jobs".
3. Subsequent pushes find the image already in GHCR — no race.

This is also explained in the header comment of `ci.yml`.

## Creating a new maintenance branch

When a new Gramps minor series goes into development and addons need
a corresponding branch (e.g. `maintenance/gramps62` once 6.2 opens):

```
git branch maintenance/gramps62 maintenance/gramps61
git push origin maintenance/gramps62
```

No workflow edits required. The workflows derive everything from
`github.ref_name`. The first push fires the same race described
above — re-run the failed CI jobs once `Build Docker Images`
finishes.

The setup job's regex (`gramps[0-9][0-9]`) requires a two-digit
suffix. When Gramps 10.0 opens this regex needs updating in two
places (`ci.yml` setup job and `docker-build.yml` params step).

## When a Gramps minor release lands on PyPI

The hybrid Dockerfile auto-detects PyPI availability:

- `pip install "gramps==X.Y.*"` succeeds → image installs the tagged
PyPI release (`::notice::` log line).
- pip reports "No matching distribution found" → image falls back to a
SHA-pinned `git clone` of `gramps-project/gramps@maintenance/grampsNN`
at the SHA captured by `docker-build.yml`'s params step
(`::warning::` log line).

When `gramps==6.1.0` is finally published to PyPI, the next image
rebuild on `maintenance/gramps61` silently switches from "git tip" to
"PyPI release" — no maintainer action needed.

To **immediately** rebuild against the new PyPI release without
waiting for the next push:

1. Open the `Build Docker Images` workflow in the Actions tab.
2. "Run workflow" → select `maintenance/grampsNN` → Run.

The `::notice::` line in the build log confirms the switch.

## Diagnostic log markers

When investigating a CI failure, the install-step output in
`Build Docker Images` carries these annotations:

| Annotation | Meaning |
| --- | --- |
| `::notice::installed gramps==X.Y.* from PyPI` | Released-path install. CI is testing against a tagged release. |
| `::warning::no gramps==X.Y.* on PyPI; installing from gramps-project/gramps@maintenance/grampsNN at <sha>` | Unreleased-branch fallback. CI is testing against the upstream branch tip at `<sha>`. Visible to contributors via the CONTRIBUTING.md note. |
| `::error::no gramps==X.Y.* on PyPI and GRAMPS_FALLBACK_SHA is unset` | `git ls-remote` in `docker-build.yml`'s params step returned no SHA for `maintenance/grampsNN` on `gramps-project/gramps`. The matching upstream branch is missing, or the addons-source branch is misnamed. |
| `::error::pip install gramps failed (non-version reason)` | pip failed for a network/registry reason, not because the version is missing. Captured stderr is dumped after the line. The build does **not** fall back to git in this case — by design, so a transient PyPI hiccup cannot silently flip a released branch into "git tip" mode. |

Other useful entry points:

- `ci.yml` setup job log shows the derived `branch_suffix` and
`ci_image`. A failure here means the branch name doesn't match
`maintenance/gramps[0-9][0-9]`.
- `docker-build.yml` params step log shows the captured
`fallback_sha`. An empty value means upstream `gramps-project/gramps`
has no matching maintenance branch (warning issued; image build will
fail iff the fallback is needed).

## Optional future-proofing knobs

Not needed today; record here in case they ever come up.

### Upstream gramps repo URL

The Dockerfile hardcodes `https://github.com/gramps-project/gramps.git`
as the fallback source. If the project ever reorgs or renames, this
URL must change in one place (`.github/docker/gramps-ci/Dockerfile`).
It could be parameterised as a build arg (`ARG GRAMPS_UPSTREAM_REPO`)
with the current URL as default, but a hardcoded value keeps the
Dockerfile simpler and a rename is a sufficiently large event that
editing one string is not the bottleneck.

### GHCR tag retention

`docker-build.yml` pushes both a moving `gramps-ci:<suffix>` tag (e.g.
`gramps60`) and a per-commit `gramps-ci:<suffix>-<short-sha>` tag.
The moving tag is always overwritten; the SHA tags accumulate over
time. Set a retention policy on the GHCR package settings page if
the count becomes inconvenient — the moving tags are what CI consumes.
116 changes: 116 additions & 0 deletions .github/docker/gramps-ci/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# .github/docker/gramps-ci/Dockerfile
#
# Gramps CI image. The Gramps minor series (6.0, 6.1, …) is picked at
# build time via the GRAMPS_SERIES build arg, so the same Dockerfile
# produces gramps-ci:gramps60, gramps-ci:gramps61, … without per-branch
# edits. Includes everything jobs need:
# - Python + pip-installed Gramps, PyGObject, pycairo
# - GTK typelibs (so addon modules that `from gi.repository import Gtk`
# at module load time are importable — widgets still need xvfb to render)
# - intltool/gettext/git for make.py builds
# - ruff, dbf for lint/test tooling (tests use stdlib unittest per AGENTS.md)
# - xvfb + xauth for tests that actually render (wrap with `xvfb-run`)
#
# No display server runs by default — the image is headless unless a command
# explicitly invokes `xvfb-run`. When running with docker locally, pass
# `--init` (or use a container runtime that injects tini) because xvfb-run
# hangs if it inherits PID 1.
#
# Gramps install path. PyPI is tried first; when no gramps==${SERIES}.*
# release exists (e.g. while 6.1 is still in development) the build
# falls back to a SHA-pinned git clone of
# gramps-project/gramps@maintenance/gramps${SERIES_NODOT}. Other pip
# failures (network, etc.) are NOT silently retried so a transient
# PyPI outage cannot flip a normally-released branch to "test against
# moving tip" mode. docker-build.yml captures the upstream SHA via
# git ls-remote and passes it in as GRAMPS_FALLBACK_SHA; the SHA
# becomes part of the buildx cache key so a moved upstream tip
# actually re-runs the install layer.
#
# Local build (released series):
# docker build --build-arg GRAMPS_SERIES=6.0 .github/docker/gramps-ci
#
# Local build (unreleased series, e.g. 6.1):
# sha=$(git ls-remote https://github.com/gramps-project/gramps.git \
# refs/heads/maintenance/gramps61 | awk '{print $1}')
# docker build --build-arg GRAMPS_SERIES=6.1 \
# --build-arg GRAMPS_FALLBACK_SHA=$sha \
# .github/docker/gramps-ci
#
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim

# Gramps minor series to install (e.g. 6.0, 6.1). No default — must be
# passed explicitly so a wrong default cannot silently produce an image
# for the wrong Gramps series. docker-build.yml derives this from the
# branch ref.
ARG GRAMPS_SERIES
# Commit SHA on gramps-project/gramps@maintenance/grampsNN. Only used
# when no gramps==${GRAMPS_SERIES}.* release exists on PyPI. Ignored
# otherwise. Empty default is intentional — on released branches the
# fallback never fires.
ARG GRAMPS_FALLBACK_SHA=""
RUN [ -n "$GRAMPS_SERIES" ] || { echo "GRAMPS_SERIES is required (e.g. 6.0)"; exit 1; }

LABEL org.opencontainers.image.source="https://github.com/gramps-project/addons-source"
LABEL org.opencontainers.image.description="Gramps ${GRAMPS_SERIES} CI image (Python, Gramps, GTK typelibs, xvfb)"

RUN apt-get update && apt-get install -y --no-install-recommends \
libgirepository-2.0-dev \
gir1.2-glib-2.0 \
gir1.2-gtk-3.0 \
gir1.2-pango-1.0 \
gir1.2-gdkpixbuf-2.0 \
gir1.2-atk-1.0 \
gir1.2-gexiv2-0.10 \
gcc \
pkg-config \
python3-dev \
libcairo2-dev \
intltool \
gettext \
git \
xvfb \
xauth \
&& rm -rf /var/lib/apt/lists/*

# Addon runtime deps (dbf, networkx, lxml, svgwrite, boto3, etc.) are
# NOT baked in here — ci.yml's "Install addon runtime deps (derived from
# requires_mod)" step pip-installs them at CI runtime from every
# .gpr.py's requires_mod list, matching what Gramps' Addon Manager does
# for an end user. Keeps .gpr.py the single source of truth.
RUN pip install --no-cache-dir PyGObject pycairo orjson ruff

# Install gramps: PyPI first, SHA-pinned git clone as fallback.
RUN /bin/bash -eo pipefail <<'BASH'
suffix_nodot="${GRAMPS_SERIES//./}"
if pip install --no-cache-dir "gramps==${GRAMPS_SERIES}.*" 2>/tmp/pip.err; then
echo "::notice::installed gramps==${GRAMPS_SERIES}.* from PyPI"
elif grep -q "No matching distribution found for gramps==" /tmp/pip.err; then
if [ -z "${GRAMPS_FALLBACK_SHA}" ]; then
echo "::error::no gramps==${GRAMPS_SERIES}.* on PyPI and GRAMPS_FALLBACK_SHA is unset"
exit 1
fi
echo "::warning::no gramps==${GRAMPS_SERIES}.* on PyPI; installing from gramps-project/gramps@maintenance/gramps${suffix_nodot} at ${GRAMPS_FALLBACK_SHA}"
mkdir -p /tmp/gramps
cd /tmp/gramps
git init -q
git remote add origin https://github.com/gramps-project/gramps.git
git fetch --depth 1 origin "${GRAMPS_FALLBACK_SHA}"
git checkout FETCH_HEAD
pip install --no-cache-dir .
cd /
rm -rf /tmp/gramps
else
echo "::error::pip install gramps failed (non-version reason); aborting rather than silently switching to git tip"
cat /tmp/pip.err
exit 1
fi
BASH

RUN apt-get purge -y gcc python3-dev pkg-config && apt-get autoremove -y

RUN python -c "from gramps.gen.const import VERSION; print('Gramps', VERSION)" \
&& python -c "import gi; gi.require_version('Gtk', '3.0'); from gi.repository import Gtk; print('GTK OK')"

WORKDIR /workspace
15 changes: 15 additions & 0 deletions .github/environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: addons-ci
channels:
- conda-forge
dependencies:
- python=3.12
- pygobject
- gtk3
- pip
# Addon runtime deps (dbf, networkx, lxml, svgwrite, boto3, etc.) are
# installed at CI runtime by ci.yml's auto-derive step from .gpr.py
# requires_mod — single source of truth. Keep only the stable base
# here (Gramps + orjson for plugin registration).
- pip:
- "gramps>=6.0,<6.1"
- orjson
Loading