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.
The audience is a Linux system engineer with 30 seconds to decide whether an update is worth it. Write for that reader:
- Lead with highlights. Begin every release section with three to five sentences of running text, directly below the version heading and above the first
###section. Cover what drives the update decision, including any manual step it requires. No bullet list, no issue links, no repetition of the individual entries. A release with only a handful of entries does not need one, since the entries themselves already fit on a screen. - State the change before its scope. Up to five affected components keep the
component: what changedform. From six on, put the statement first and close it with either a collective name (all *-version checks) or the components in parentheses, so the entry is understood from its first line. These broad entries come first in their subsection, ahead of the alphabetically sorted per-component entries. - One sentence per entry.
Added,ChangedandFixedsay what an administrator notices. Root cause, reproduction steps and internal reasoning belong in the commit body and the issue. - Migration instructions only under
Breaking Changes. Wording such as "rename x to y" or "set z to restore the previous behaviour" anywhere else means the entry sits in the wrong section. Entries underBreaking Changesmay run longer than one sentence. - Leave out contributor-only changes. Lockfile and pin bumps, Dependabot and pre-commit configuration, GitHub Actions bumps and test infrastructure are covered by the git history and the pull request. Keep an entry only where an administrator sees the effect, for example when it changes the released artifact.
A release section starts like this:
## [v6.1.0] - 2026-09-15
**Highlights:** Two long-standing sources of false alarms are gone, and container workloads are now covered. Cumulative counters are reported as rates instead of totals, so any dashboard built on them has to be re-imported.
### AddedThe scope rule, on an entry affecting 43 components. Instead of:
* about-me, borgbackup, deb-lastactivity, file-ownership, fs-xfs-stats, getent, ...: `--always-ok` to force an OK resultwrite:
* `--always-ok` forces an OK result on 43 further components (about-me, borgbackup, deb-lastactivity, ...)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.- Every other tool a workflow installs with
pip(ansible-builder,build,mkdocs,pdoc,ruff,tox, ...) follows the same model: a version pin in.github/<name>/requirements.in, a hash-pinnedrequirements.txtgenerated from it the same way,pip install --require-hashes --requirement .github/<name>/requirements.txtin the workflow, and a Dependabotpipentry for that directory. Dependabot does not readrun:lines, so a version pinned there (package==X.Y.Z) is never updated, and a ScorecardpipCommand not pinned by hashfinding on it is a real one.
- 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 only updates the manifest; the build is started by hand afterwards. - Publish: not implemented yet.
build/publish.shis meant to upload the built packages into the Linuxfabrik Pulp repositories, served underrepo.linuxfabrik.ch, but the Pulp server it targets is not deployed, so the script stops with an error. Until then, a build ends with its artifacts. - Release trigger (first-party): planned, not implemented. A Linuxfabrik product's
own release workflow is meant to fire a
repository_dispatchto this repository with the package name and version, which would run the matrix build and publish. No workflow here listens forrepository_dispatchyet.
The intended flow, not implemented yet: 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/.