Skip to content

Release process

This document describes the current release path for The Merovingian.

Alpha prereleases

Alpha packages are published from Git tags that match this pattern:

v*-alpha*

Pushing a matching tag triggers release.yml. That workflow:

  • builds hardened packages for Linux and FreeBSD (linux-alpha-package, freebsd-alpha-package) — OpenBSD and NetBSD packages are not part of the alpha release workflow; they are built by the rolling packages.yml workflow and published to its latest GitHub release instead
  • runs the full Meson test suite on both platforms
  • runs Linux phase 1 configuration validation
  • runs the unsafe-source gate
  • runs release-readiness metadata checks
  • packages merovingian-server and merovingian-db-migrate
  • attaches SHA-256 checksum files
  • collects release evidence per platform (scripts/collect-release-evidence.sh): compiler version, link-time hardening confirmation (scripts/check-elf-hardening.sh), pinned dependency versions from subprojects/*.wrap, the test log's Ok/Fail/Timeout summary, and the mandatory fuzz target list — folded into the published release notes alongside the package checksums
  • publishes a GitHub prerelease with the built assets

Each tarball carries a SLSA provenance attestation generated by actions/attest-build-provenance and verifiable with:

gh attestation verify <tarball> --repo OMG-Software/Merovingian

Published releases also trigger sbom.yml, which generates SPDX and CycloneDX JSON SBOM files and attaches them to the GitHub release as merovingian.spdx.json and merovingian.cdx.json. Repository changes are additionally covered by:

The packaged tarballs include:

  • bin/merovingian-server
  • bin/merovingian-db-migrate
  • config/merovingian.conf.example
  • docs/user-manual.md
  • docs/release-process.md
  • docs/security-review-checklist.md
  • Linux and BSD packaging scaffolds, including Debian control metadata, RPM spec metadata, FreeBSD manifest metadata, OpenBSD packing-list metadata, and NetBSD/pkgsrc Makefile metadata
  • README.md
  • LICENSE

Starting a minor series: the upgrade fixture

Upgrades are supported from 1.0.0 onward, so this applies to minor series from 1.1 on. A database written by the previous supported minor series must open, migrate and serve under every candidate (ADR-0216). When the first version of such a series is cut (for example 1.1.0), add the fixture of the last release of the series before it (there, the last 1.0.x release):

python scripts/generate_upgrade_fixture.py <last-release-commit> tests/fixtures/upgrade/<last-release-version>

Then add scenarios to tests/integration/test_database_upgrade_flow.cpp that open that fixture and serve it, and remove the previous fixture directory. Never regenerate a fixture from current code: it is evidence only because the old release wrote it.

A pre-1.0 series start (such as 0.13.0) adds no fixture and no upgrade test. The migrations were collapsed into one initial schema in 0.12.47 (ADR-0224), so a pre-1.0 database can no longer be opened: that change removed the 0.11.13 fixture and its scenarios, and there is no fixture until the 1.1 series starts.

The v1 schema is frozen from 1.0.0

From 1.0.0, the v1 migration (the initial_schema step in src/database/migration.cpp) and migrations/001_initial_schema.sql never change. A schema change is a new numbered migration (v2 onwards) that keeps deployed databases compatible. A unit test requires the SQL file to match the build's v1 step statement for statement.

Verifying release signatures

Every release tarball and package is accompanied by a detached GPG signature (.asc). The signing key is an offline, long-lived maintainer key stored as a GitHub Actions secret. Operators can verify artifacts without the GitHub CLI.

gpg --keyserver keys.openpgp.org --recv-keys 66DFCC50187C8E46B5ED85FD92A3A264F0A7BE20

Verify a release artifact:

gpg --verify merovingian-<version>-linux-x86_64.tar.gz.asc \
  merovingian-<version>-linux-x86_64.tar.gz

If the signature is valid, GPG reports a good signature from the maintainer identity. A missing or invalid signature must be treated as a build or distribution failure.

Release assets also include:

  • SHA256SUMS for every package (in rolling latest releases)
  • per-tarball .sha256 files for alpha releases
  • merovingian.spdx.json and merovingian.cdx.json SBOMs
  • merovingian.licenses.json machine-readable license summary

Operator sequence

The release operator should still run the normal local evidence path before creating the tag:

sh scripts/setup-dev-env.sh --check-only
meson setup build -Dbuild_tests=true -Dbuild_fuzz=true
meson compile -C build
meson test -C build --print-errorlogs
sh scripts/reject-unsafe.sh
sh scripts/check-release-readiness.sh

After that, create and push an alpha tag:

git tag v<version>-alpha.1
git push origin v<version>-alpha.1

GitHub Actions publishes the prerelease assets for that tag. The matching release event also publishes the SBOM assets, and the scheduled dependency-triage workflow keeps a separate vulnerability report artifact.

Rolling latest packages

Pushes to main also trigger packages.yml. That workflow rebuilds packages for all supported platforms, replaces the rolling latest GitHub prerelease, and uploads fresh package checksums:

Artifact Job
Ubuntu .deb deb
Debian trixie .deb debian-pkg
Fedora .rpm rpm
RHEL-compatible .rpm (AlmaLinux 10) rhel-rpm
OpenSUSE Tumbleweed .rpm opensuse-rpm
FreeBSD .pkg freebsd-pkg
OpenBSD .tgz openbsd-pkg
NetBSD .tgz netbsd-pkg (retried once on QEMU infrastructure failure)
Portable static Linux tarball (musl) static-linux-fallback

Every packaging script configures Meson with --buildtype=release. Meson's default debug build has no _FORTIFY_SOURCE, and the server refuses to start when its startup hardening self-check cannot confirm it (ADR-0148). A new packaging path must do the same, and be added to PACKAGING_SCRIPTS in tests/tooling/test_packages_workflow.py.

Before attesting, the deb and debian-pkg jobs install the package they built and run scripts/check-startup-hardening-gate.sh against the installed /usr/bin/merovingian-server, as the merovingian user with an empty capability bounding set. The job fails unless every startup hardening check reports enabled and the server stops cleanly. The docker-image job does the same for the Dockerfile: it builds the image and runs the gate inside it with --cap-drop=ALL --security-opt=no-new-privileges. The image is not published; the job is a gate on the recipe.

The Dockerfile pins debian:trixie-slim by the digest of its multi-architecture index. To move to a newer base, read the current digest (for example docker buildx imagetools inspect debian:trixie-slim, the Digest: line of the index, not a per-platform manifest), replace it in both FROM lines, and let the docker-image job prove the new image still passes the gate.

Each package is individually attested with actions/attest-build-provenance before upload, verifiable with:

gh attestation verify <package-file> --repo OMG-Software/Merovingian

The publish step also creates a detached GPG .asc signature for every package (including SHA256SUMS) using the maintainer key. Verify with:

gpg --verify <package-file>.asc <package-file>

The publish step resolves and deletes the existing latest release with an explicit --repo "${{ github.repository }}" scope before recreating it so the artifact-only job does not depend on checkout state.

The static Linux fallback artifact is built on Alpine/musl with -static-pie and packaged as merovingian-<version>-linux-static-x86_64.tar.gz. It is a portability fallback for older Linux distributions that cannot easily consume the .deb or .rpm; distro packages remain preferred where available because their OpenSSL, libsodium, and libpq dependencies receive normal OS security updates.