Development¶
Contributions are welcome! See the organisation's contributing guide, and mind our code of conduct.
Tests¶
Dependencies are managed with uv: pyproject.toml
lists them (the runtime dependencies, and the dependency groups dev for
tests, linters and type checks, and docs for the documentation), and
uv.lock pins them; the image installs the locked runtime dependencies. After
changing dependencies, run uv lock (CI checks that the lock matches). The
app runs from its source directory, not as an installed package.
make test installs the locked dependencies into .venv (uv sync, with
Python 3.13, the version of the base image) and runs the tests with
coverage (written to coverage.xml; 100% coverage is required). Without
make:
The tests need no network access and no database. Unit tests mock the
database (mongomock) and clone
a local repository instead of one on GitHub (Git rewrites the URL, see
tests/conftest.py). The integration tests in tests/integration/ run
the app as deployed, with response validation, and test registering,
publishing and notifying through its API, with a local subscriber. They
use mongomock too, unless MONGODB_TEST_URI is set: then they use the
MongoDB at that URI, which must not have a database pubgradeStore (the
tests drop it before each test). CI runs all tests with mongomock, and the
integration tests also against MongoDB 7.0; to do the latter locally:
docker run -d --rm --name pubgrade-test-mongo -p 127.0.0.1:27017:27017 mongo:7.0
MONGODB_TEST_URI=mongodb://localhost:27017 uv run pytest tests/integration
docker stop pubgrade-test-mongo
The app is built on FOCA 0.21: its
configuration is pubgrade/config.yaml, and pubgrade/app.py creates the app
(create_app()). The image serves it with Gunicorn, with FOCA's settings
and more threads and shutdown time (pubgrade/gunicorn_config.py);
python -m pubgrade.app runs Flask's development server instead. FOCA is
bumped by hand: the tag and digest of the base image in the Dockerfile
and foca in pyproject.toml and uv.lock together, following FOCA's
migration guides; tests/test_image.py fails if they don't match.
tests/smoke.sh <base URL> checks the API of a running Pubgrade with an empty
database, and tests/chart.sh the rendered chart.
tests/e2e/upgrade.sh tests the whole path, and needs Docker, Helm and jq: it
starts a throwaway k3s cluster, installs Pubgrade from its chart, sets up two
namespaces with a Deployment and a sidecar each, publishes one image and checks
that both roll out. It also covers an unreachable subscriber and a tag that
doesn't exist, which takes a few minutes, as sidecars retry first. Sidecars are
built from the branch dev of
Pubgrade-Sidecar; set
SIDECAR_IMAGE to use a local image instead.
Lint¶
Code style, type hints and the absence of secrets are checked with
pre-commit (.pre-commit-config.yaml):
- Ruff lints and formats the Python code
(Black style; rules in
[tool.ruff]ofpyproject.toml), including Google-style docstrings for all modules, classes and functions. - mypy checks the type hints, which must be
complete (
[tool.mypy]). - gitleaks looks for secrets in the staged changes; CI also scans all files.
- actionlint checks the GitHub workflows, and further hooks the YAML syntax, whitespace and file endings.
make lint runs all hooks on all files, including the scan of all files
for secrets, as CI does. To run them on each commit, install them once:
uv sync --all-groups # mypy also checks the hook of the docs build
uv run pre-commit install
uv run pre-commit run --all-files # without make
The Sidecar's submodule in external/ is left out. Formatting changes that
touch many lines go in a commit of their own, listed in
.git-blame-ignore-revs, so that git blame skips them; for GitHub's blame
view this works as is, locally after
git config blame.ignoreRevsFile .git-blame-ignore-revs.
CI¶
CI
runs on pull requests and pushes to dev: the pull request title (Conventional
Commits, at most 50 characters, as it becomes the commit message of the squash
merge), lint (the pre-commit hooks, see Lint, and the citation
metadata in CITATION.cff), the tests (with mongomock, with coverage to
Codecov; the integration tests also against MongoDB 7.0), the chart, the smoke
and upgrade tests, the documentation, and a scan of the images. The scan fails
on critical and high vulnerabilities that have a fix in the app's image; the
updater's image, which isn't built on FOCA, is only reported for now. The job
CI passed sums up all other jobs and is the only required check.
Pushes to dev publish elixircloud/pubgrade and
elixircloud/pubgrade-updater, signed with Sigstore and tagged dev and
sha-<commit>; releases tag X.Y.Z and latest (see
Versioning and releases).
Publishing runs in the environment dev-images (branch dev only), with the
Docker Hub secrets DOCKERHUB_LOGIN and DOCKERHUB_TOKEN; the variable
DOCKERHUB_ORG sets the Docker Hub organization (by default elixircloud),
and the secret CODECOV_TOKEN is for the coverage report. Verify an image's
signature with cosign:
cosign verify elixircloud/pubgrade:dev \
--certificate-identity-regexp '^https://github\.com/elixir-cloud-aai/Pubgrade/\.github/workflows/ci\.yml@refs/heads/dev$' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Documentation¶
The documentation is built with MkDocs and
Material for MkDocs from
docs/ and mkdocs.yml, and published on Read the
Docs (.readthedocs.yaml). The README's
synopsis and quick start are included in the home page, between
--8<-- [start:<name>] and --8<-- [end:<name>] markers in README.md.
The section Pubgrade-Sidecar is the documentation of
Pubgrade-Sidecar, which
keeps it in its own repository (docs/, built strictly in its own CI). It is
included from the Git submodule external/pubgrade-sidecar, pinned to a commit
of the Sidecar's branch dev, by the hook docs/hooks.py: it adds the
Sidecar's pages under sidecar/, in the order of the Sidecar's mkdocs.yml.
Dependabot proposes updates of the submodule monthly; to update it by hand:
To build the documentation as CI and Read the Docs do, with the submodule checked out:
git submodule update --init
uv run --only-group docs mkdocs build --strict # or `mkdocs serve` to preview
The build fails on warnings, e.g. of broken links.
Versioning and releases¶
The project follows semantic versioning. It is in
beta, so the API may change without notice. The app, its two images and its
chart are released together, with one version: the tag vX.Y.Z (or
vX.Y.ZaN, vX.Y.ZbN and vX.Y.ZrcN for pre-releases), version in
[project] of pyproject.toml, the chart's appVersion and version
(the SemVer form, e.g., 1.2.3-rc1 for 1.2.3rc1), and the version in
CITATION.cff.
Until 1.0.0, all releases are GitHub pre-releases.
For the first release, the chart's version (0.2.0, from before the release
workflow) and the app's version (0.1.0) differ; the release-prep pull request
sets both, e.g., to 0.2.0.
- Release-prep pull request to
dev: set the version inpyproject.toml(then runuv lock),helm/Chart.yaml(versionandappVersion) andCITATION.cff(python scripts/citation.py set <version>, which also sets the release date). - Once CI has passed for its commit on
dev, bring that commit tomain. - Tag it on
mainwith a signed, annotated tag and push the tag:git tag -s v<version> -m v<version>, thengit push origin v<version>.
The release workflow
checks the tag (a release version, annotated, with a signature that GitHub
verified, on main), that the versions above match it
(scripts/check_versions.py and scripts/citation.py check) and that CI
passed for a push of the commit. It packages the chart, with the images of
the release as defaults (elixircloud/pubgrade:X.Y.Z and
elixircloud/pubgrade-updater:X.Y.Z), pushes both images by digest, scans
them, then attests and signs the images and the chart, tags the images
X.Y.Z (and latest if it is the highest stable release;
scripts/release_version.py) and creates the GitHub release, with the chart
and its Sigstore bundle attached. Pushing the images and publishing the
release each need approval in the environment release, which holds the
Docker Hub secrets DOCKERHUB_LOGIN and DOCKERHUB_TOKEN. Verify a released
image's signature with:
cosign verify elixircloud/pubgrade:<version> \
--certificate-identity-regexp '^https://github\.com/elixir-cloud-aai/Pubgrade/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The release gets GitHub's generated notes; for a first release (no published
release before it), scripts/release_notes.py builds them instead, because
without a previous tag GitHub starts at an older tag of its own choice. Edit
the notes by hand into the agreed format after publishing: the notes of
immutable releases stay editable, their assets don't. So the release is
created as a draft, gets its assets and is published last
(scripts/release_assets.py); a re-run of failed jobs finishes it. Don't
re-run all jobs of a release that was published: that builds new image
digests and moves the release's image tags to them before it fails at the
release.
The images are scanned for known vulnerabilities with Trivy: in CI, in the
release workflow before the images are signed and tagged, and weekly for
latest
(scan.yml).
The scans of the app's image in CI and of releases fail on critical and high
vulnerabilities that have a fix; accept a finding only with a reason and an
expiry date, in .trivyignore.yaml. The updater's image, which isn't built
on FOCA, is reported only. The weekly scan doesn't fail until the first
release, as latest is still an image from January 2025 on FOCA 0.7; once
the first release has published latest, drop fail: "false" there for
pubgrade so that it fails too.
The scripts in scripts/ use only the standard library and are tested in
tests/scripts/, with the app's coverage.