Skip to content

Latest commit

 

History

History
321 lines (220 loc) · 16 KB

File metadata and controls

321 lines (220 loc) · 16 KB

Contributing

Linuxfabrik Standards

The following standards apply to all Linuxfabrik repositories.

Code of Conduct

Please read and follow our Code of Conduct.

Issue Tracking

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): ...).

Pre-commit

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 install

Commit Messages

Commit 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 libraries
  • docs: Documentation only changes
  • feat: A new feature
  • fix: A bug fix
  • perf: A code change that improves performance
  • refactor: A code change that neither fixes a bug nor adds a feature
  • style: Changes that do not affect the meaning of the code (whitespace, formatting, etc.)
  • test: Adding missing tests

Changelog

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 changed form. 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, Changed and Fixed say 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 under Breaking Changes may 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.

### Added

The 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 result

write:

* `--always-ok` forces an OK result on 43 further components (about-me, borgbackup, deb-lastactivity, ...)

Language

Code, comments, commit messages, and documentation must be written in English.

CI Supply Chain

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-commit is installed from a hash-pinned requirements file at .github/pre-commit/requirements.txt, generated with pip-compile --generate-hashes --strip-extras from .github/pre-commit/requirements.in. Dependabot's pip ecosystem 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-pinned requirements.txt generated from it the same way, pip install --require-hashes --requirement .github/<name>/requirements.txt in the workflow, and a Dependabot pip entry for that directory. Dependabot does not read run: lines, so a version pinned there (package==X.Y.Z) is never updated, and a Scorecard pipCommand not pinned by hash finding on it is a real one.

Coding Conventions

  • 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-00 through 00-00-5E-00-53-FF (unicast), 01-00-5E-90-10-00 through 01-00-5E-90-10-FF (multicast)
    • Domains: *.example, example.com

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.

Concepts

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/.

Package formats

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-* and sles-*. Built with rpmbuild; build dependencies are resolved with dnf builddep on EL/Fedora and with a zypper fallback on SLES.
  • DEB (PKG_DEB_DEBIAN_DIR, PKG_DEB_DISTROS): debian-*, ubuntu-*. Built with debuild.

Why native recipes, not FPM

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.

The manifest (package.conf)

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.ch

The spec uses %{pkg_version} / %{pkg_release}, injected by create-rpm.sh, so the version lives only in the manifest.

Distro-native builds

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-python has 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.

Base third-party recipes on the official packaging

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.

Split packages for GUI applications

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.

Adding a package

  1. Create packages/<name>/ with the manifest and the recipes you need (rpm/, deb/).

  2. Pick the smallest set of PKG_RPM_DISTROS / PKG_DEB_DISTROS that the package actually needs. Not every package targets every distro or format.

  3. 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
  4. Open a pull request.

Build and publish pipeline

  • 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.conf PKG_VERSION against 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.sh is meant to upload the built packages into the Linuxfabrik Pulp repositories, served under repo.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_dispatch to this repository with the package name and version, which would run the matrix build and publish. No workflow here listens for repository_dispatch yet.

Publishing to Pulp

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>, then pulp rpm publication create --repository=<repo>.
  • DEB: pulp deb content upload --repository=<repo> --file=<deb>, then pulp 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.

Code style

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/.