The following standards apply to all Linuxfabrik repositories.
Please read and follow our Code of Conduct.
Open issues are tracked on GitHub Issues in the respective repository. In addition to the GitHub default labels (bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix), the following project-specific labels are used:
| Label | Use for |
|---|---|
build |
Packaging, build scripts, distribution artifacts. |
ci/cd |
Continuous integration, GitHub Actions workflows, release automation, test automation. |
dependencies |
Pull requests opened by Dependabot. |
github_actions |
Pull requests that update GitHub Actions workflow definitions or pinned action SHAs. |
python |
Pull requests that update Python dependencies. |
When opening a new issue, attach the label that matches the area of work. The build and ci/cd labels mirror the conventional commit scopes used in the same areas (fix(build): ..., chore(ci/cd): ...).
Some repositories use pre-commit for automated linting and formatting checks. If the repository contains a .pre-commit-config.yaml, install pre-commit and configure the hooks after cloning:
pre-commit installCommit messages follow the Conventional Commits specification:
<type>(<scope>): <subject>
If there is a related issue, append (fix #N):
<type>(<scope>): <subject> (fix #N)
<type> must be one of:
chore: Changes to the build process or auxiliary tools and librariesdocs: Documentation only changesfeat: A new featurefix: A bug fixperf: A code change that improves performancerefactor: A code change that neither fixes a bug nor adds a featurestyle: Changes that do not affect the meaning of the code (whitespace, formatting, etc.)test: Adding missing tests
Document all changes in CHANGELOG.md following Keep a Changelog. Sort entries within sections alphabetically.
Code, comments, commit messages, and documentation must be written in English.
GitHub Actions in .github/workflows/ are pinned by commit SHA, not by tag. Dependabot's github-actions ecosystem keeps these pins up to date.
Python packages installed via pip inside workflows follow a two-tier policy:
pre-commitis installed from a hash-pinned requirements file at.github/pre-commit/requirements.txt, generated withpip-compile --generate-hashes --strip-extrasfrom.github/pre-commit/requirements.in. Dependabot'spipecosystem watches that directory and maintains both files.- One-shot installs such as
ansible-builder,build,mkdocs,pdoc, andruffin release, docs, or test workflows are version-pinned only (package==X.Y.Z) and kept fresh by Dependabot. Scorecard'spipCommand not pinned by hashfindings for these are considered acceptable risk and may be dismissed.
- Sort variables, parameters, lists, and similar items alphabetically where possible.
- Always use long parameters when using shell commands.
- Use RFC 5737, 3849, 7042, and 2606 in examples and documentation:
- IPv4:
192.0.2.0/24,198.51.100.0/24,203.0.113.0/24 - IPv6:
2001:DB8::/32 - MAC:
00-00-5E-00-53-00through00-00-5E-00-53-FF(unicast),01-00-5E-90-10-00through01-00-5E-90-10-FF(multicast) - Domains:
*.example,example.com
- IPv4:
This repository builds Linuxfabrik RPM and DEB packages from both Linuxfabrik's own products and third-party upstream software, and publishes them on repo.linuxfabrik.ch. It is open to an arbitrary number of packages.
A package is one piece of software we ship, living under packages/<name>/.
Each package brings:
package.conf: the manifest, sourced by the build scripts. It declares the source, the version to package and the per-format target distributions.rpm/<name>.spec: the RPM recipe (EL, Fedora, SLES).deb/debian/: the DEB recipe (control,rules,copyright,source/format).files/: assets shared between the formats, e.g. the systemd unit.
A package only ships the recipes for the formats it targets. The build is
package-agnostic: build/create-package.sh reads the manifest, fetches the
source and dispatches to create-rpm.sh or create-deb.sh depending on the
target distro. matrix-package.sh runs that across many distros,
each inside the matching container from build/containerfiles/.
Each package.conf declares, per format, the recipe and the distro targets it applies to. A
package only targets the formats and distros it needs:
- RPM (
PKG_RPM_SPEC,PKG_RPM_DISTROS): EL (rocky-*,rhel-*,almalinux-*,centos-*),fedora-*andsles-*. Built withrpmbuild; build dependencies are resolved withdnf builddepon EL/Fedora and with a zypper fallback on SLES. - DEB (
PKG_DEB_DEBIAN_DIR,PKG_DEB_DISTROS):debian-*,ubuntu-*. Built withdebuild.
We build with each format's native tooling (rpmbuild / debuild) and hand-written recipes
rather than a cross-format generator such as FPM.
Linuxfabrik used FPM before and dropped it: it is not flexible enough for real packaging,
especially for Python virtual environments (control over the venv layout, interpreter paths,
%pyproject macro integration and precise dependency generation). Native recipes also let us
adopt an upstream or distribution recipe (a .spec or a debian/ directory) almost as-is and
base each package on the official packaging (see below), instead of reconstructing its logic
through FPM flags.
PKG_VERSION='4.5.4' # upstream release tag we package (without leading v)
PKG_GITHUB_USER='nicolargo'
PKG_GITHUB_REPO='glances'
PKG_SOURCE_TARBALL="glances-${PKG_VERSION}.tar.gz"
PKG_SOURCE_URL="https://github.com/${PKG_GITHUB_USER}/${PKG_GITHUB_REPO}/archive/v${PKG_VERSION}/${PKG_SOURCE_TARBALL}"
PKG_RPM_SPEC='rpm/glances.spec'
PKG_RPM_DISTROS='rocky-v10 fedora-v44' # EL and Fedora targets
PKG_DEB_DEBIAN_DIR='deb/debian'
PKG_DEB_DISTROS='debian-v13 ubuntu-v2404'
PKG_REPO_SUBDIR='glances' # sub-directory under repo.linuxfabrik.chThe spec uses %{pkg_version} / %{pkg_release}, injected by create-rpm.sh,
so the version lives only in the manifest.
The default build model is distro-native: recipes use each format's standard toolchain
(pyproject-rpm-macros / pybuild), and the runtime dependencies (and, for GUI apps, PySide6/Qt)
are pulled from the target distribution. This keeps packages small (noarch where possible) and
lets the distribution own the interpreter and the dependency stack.
We build one package file per distro and version, never a single universal artifact: RPMs carry
the distribution's %{?dist} tag (...-1.el10.noarch.rpm, ...-1.fc44.noarch.rpm) and DEBs are
built per suite. Each distro+version is published into its own repository tree, because dependency
names and versions differ per release.
A product can therefore only target distributions that ship a recent enough interpreter and dependency versions. Verify this per product, in a container, before declaring a target. Two recurring pitfalls:
- Interpreter version. A product's
requires-pythonhas to be met by a distro-provided interpreter. Pin it as low as the code truly needs, not aspirationally. Distros lag. - Dependency versions. Many current distros still ship SQLAlchemy 1.4, not 2.0, and do not
package niche libraries at all. Loosen the product's pins to floors (
>=) so distro versions satisfy them, and drop targets that cannot provide a required major version.
Distributions that are too old for distro-native are simply not targeted. Covering them would require a heavier build mode that bundles the interpreter and dependencies; out of scope until a product actually needs it.
For third-party software, do not write a recipe from scratch. Start from the distribution's own reference packaging and adapt it to the conventions here:
- RPM: the Fedora/EPEL spec.
- DEB: the Debian package's
debian/directory.
This gets the per-distro details right, and they differ more than they look: dependency names,
the split between hard dependencies and optional ones, the license identifier and the build
steps. For example, glances' web-UI dependencies are hard requirements in the Fedora spec but
optional in another distribution's packaging. Keep only the
Linuxfabrik-specific deltas on top of the reference: the shared files/, the version injected
from package.conf, and the locally provided source tarball.
A GUI application ships as two packages from one source, so that a CLI-only install never pulls
the heavy GUI stack. The base package owns the CLI/library code and its dependencies; a separate
-gui package owns only the GUI code and depends on the distro GUI toolkit (python3-pyside6 on
EL/Debian/Ubuntu). Because the toolkit dependency lives only in the -gui
package, installing the base alone never pulls it. FirewallFabrik is the first such case
(issue #114): linuxfabrik-firewallfabrik and linuxfabrik-firewallfabrik-gui.
-
Create
packages/<name>/with the manifest and the recipes you need (rpm/,deb/). -
Pick the smallest set of
PKG_RPM_DISTROS/PKG_DEB_DISTROSthat the package actually needs. Not every package targets every distro or format. -
Build locally and verify the package installs and runs:
PKG_NAME=<name> PKG_TARGET_DISTROS='rocky-v10' PKG_PACKAGE_ITERATION=1 \ PKG_ARCH=x86_64 PKG_DIR_REPO="$(pwd)" PKG_DIR_PACKAGED="$(pwd)/packaged" \ bash build/matrix-package.sh
-
Open a pull request.
- Build: the
Linuxfabrik: Build Linux (x86_64)workflow (.github/workflows/lf-build-linux-x86_64.yml) builds a package across its configured distros, one matrix job per distro, and uploads the results as artifacts. It can be triggered manually (workflow_dispatch) or by another workflow (workflow_call). - Version detection: a scheduled workflow compares each
package.confPKG_VERSIONagainst the latest upstream release and proposes a bump when the upstream is ahead. Merging the bump triggers a build. - Publish:
build/publish.shuploads the built packages into the Linuxfabrik Pulp repositories, served underrepo.linuxfabrik.ch. - Release trigger (first-party): a Linuxfabrik product's own release workflow
fires a
repository_dispatchto this repository with the package name and version, which runs the matrix build and publish. The scheduled version-detection workflow acts as a delayed safety net.
Publishing targets a Pulp server. build/publish.sh
uploads each built package into the matching Linuxfabrik repository and triggers a new
publication:
- RPM:
pulp rpm content upload --repository=<repo> --file=<rpm>, thenpulp rpm publication create --repository=<repo>. - DEB:
pulp deb content upload --repository=<repo> --file=<deb>, thenpulp deb publication create --repository=<repo>.
Distributions are created with --repository, so the newest publication is served
immediately. Pulp signs the repository metadata itself through its signing service
(the Linuxfabrik GPG key); no package or metadata signing happens in CI, and
repo_gpgcheck=1 is enforced on clients.
Repository layout: one aggregated Linuxfabrik repository per distribution -- EL
(el/<version>/<arch>/, one per EL major, covering Rocky/Alma/RHEL/CentOS via the
client's $releasever/$basearch), Debian and Ubuntu (<debian|ubuntu>/ with
dists/<codename>-<channel>/main), and SLES (sles/<version>/<arch>/, own packages
only). Two channels: release and testing.
build/publish.sh is currently a stub, deferred on purpose: the Pulp server is not
deployed yet, and the target repositories and signing service are provisioned as part
of that rollout. The earlier per-package createrepo/freight publishing is being
retired and must not be wired here.
Arch/pacman is intentionally out of scope. Pulp ships pulp_rpm, pulp_deb and
pulp_file but no pacman plugin (verified against the Pulp source), so there is no Linuxfabrik
distribution channel for pacman packages; the packaging repo therefore does not build them.
The Linuxfabrik Standards above apply (long options, Conventional Commits, English, alphabetical sorting). On top of that:
- Shell scripts start with
set -e -o pipefail -u. - Keep the per-package recipes minimal; put anything reusable into
build/.