Skip to content

Building & Releasing Packages

This directory contains custom Debian packages for the Toolsera infrastructure, designed to be distributed via the Nexus APT repository at https://nexus.toolsera.lan/repository/toolsera/.

packages/
├── README.md # This file
├── build.sh # Package build script
└── <package-name>/ # Package directory
├── DEBIAN/
│ └── control # Package metadata
└── usr/ # Package files (mirrors installation paths)
└── local/
└── bin/
└── <script> # Executable scripts

For a full reference on build.sh — its arguments, both build modes, the package.yml schema, exit codes and troubleshooting — see debian-ubuntu/BUILD-SCRIPT.md.

  • dpkg-deb (usually pre-installed on Debian/Ubuntu)
  • Bash shell
  1. Navigate to packages directory:

    Terminal window
    cd packages/
  2. Make build script executable (first time only):

    Terminal window
    chmod +x build.sh
  3. Build a package:

    Terminal window
    ./build.sh <package-name>

    Example:

    Terminal window
    ./build.sh hello-toolsera
  4. Output: The .deb file will be created in the current directory:

    hello-toolsera_1.0.0_all.deb

Before uploading to Nexus, test the package locally:

Terminal window
# Inspect package metadata
dpkg-deb --info hello-toolsera_1.0.0_all.deb
# View package contents
dpkg-deb --contents hello-toolsera_1.0.0_all.deb
# Install package
sudo dpkg -i hello-toolsera_1.0.0_all.deb
# Test the installed command
hello-toolsera
# Remove package (if needed)
sudo apt remove hello-toolsera

Every package is described by packages/debian-ubuntu/<pkg>/package.yml (see existing packages for examples). To ship a new version:

  1. Bump the version: field in package.yml (and the Version: line in DEBIAN/control for mode: dpkg packages).
  2. Commit and merge to main.
  3. Push a tag of the form pkg/<package>-<version>, e.g.:
    Terminal window
    git tag pkg/hello-toolsera-1.0.1
    git push origin pkg/hello-toolsera-1.0.1

The .github/workflows/publish-package.yml workflow runs on the self-hosted homelab,deb-builder runner. It builds the package via build.sh, uploads it to Nexus with bws run-injected credentials, triggers an APT metadata rebuild on Nexus via the repository’s rebuild-index API, and attaches the .deb to a GitHub Release.

The same workflow can be triggered manually from the Actions tab via workflow_dispatch (provide package and version).

build.sh stages the payload and sets modes explicitly — it never inherits them from the checkout, because git carries no usable exec bit for files committed from a Windows checkout (core.fileMode=false). Defaults:

  • 0755 for anything landing in /bin, /sbin, /usr/bin, /usr/sbin, /usr/local/bin, /usr/local/sbin
  • 0644 for everything else; directories are always 0755

Override per entry with a quoted permissions: field:

files:
- src: scripts/my-tool/my-tool
dest: /usr/local/bin/my-tool
permissions: '0755'
- src: scripts/my-tool/helper.sh
dest: /usr/share/my-tool/helper.sh
permissions: '0755'

Maintainer scripts and conffiles in mode: fpm manifests

Section titled “Maintainer scripts and conffiles in mode: fpm manifests”

fpm takes maintainer scripts as flags rather than as a DEBIAN/ tree, so a package needing install-time logic does not have to drop to mode: dpkg — and can therefore keep sourcing its payload from elsewhere in the repo instead of duplicating it:

scripts:
after_install: scripts/my-tool/postinst
before_remove: scripts/my-tool/prerm
after_remove: scripts/my-tool/postrm
config_files:
- /etc/my-tool/env

before_install is supported too. Each path is checked to exist before the build starts. config_files lists installed paths dpkg should treat as conffiles, so an admin’s edits survive upgrades instead of being overwritten.

Two optional metadata fields are worth setting while you are there: url: (otherwise apt show prints fpm’s http://example.com/no-uri-given placeholder) and section: (defaults to utils; fpm’s own default is the meaningless default).

Maintainer scripts run under Debian’s /bin/sh, which is dash — check them with dash -n rather than assuming bash builtins are available.

Maintainer scripts must never prompt. apt runs them with stdin detached, so a read gets EOF or hangs while holding the dpkg lock, and unattended installs break. If a package needs answers from a human, either use debconf (a config script plus templates) or — as qbt-tracker-remover does — ship a configure subcommand the admin runs afterwards on a terminal. Prefer the latter for anything secret: debconf persists its answers in /var/cache/debconf/, which puts a second copy of the secret on disk.

Anything a maintainer script generates (a venv, a cache) is not in the package file list and dpkg will not remove it. Clean it up in postrm or it stays behind forever.

When CI is unavailable, the same scripts the workflow uses work locally:

Terminal window
cd packages/debian-ubuntu
./build.sh hello-toolsera --expected-version 1.0.1
export NEXUS_URL=https://nexus.toolsera.lan
export NEXUS_REPO=toolsera
export NEXUS_USER=admin
export NEXUS_PASS='...'
./upload.sh hello-toolsera_1.0.1_amd64.deb

Or fetch the credentials from BWS the same way CI does:

Terminal window
bws run --access-token "$BWS_ACCESS_TOKEN" -- ./upload.sh hello-toolsera_1.0.1_amd64.deb

The Nexus Web UI (Browse → Repositories → toolsera → Upload component) is also still available for one-off uploads.

Once uploaded and metadata is rebuilt, install packages on client machines:

Terminal window
# Update package list
sudo apt update
# Install package
sudo apt install hello-toolsera
# Run command
hello-toolsera
Terminal window
mkdir -p <package-name>/DEBIAN
mkdir -p <package-name>/usr/local/bin # Or other paths as needed
Package: <package-name>
Version: 1.0.0
Section: utils
Priority: optional
Architecture: all
Maintainer: Toolsera Admin <admin@toolsera.lan>
Description: Short description
Longer description here.
Can span multiple lines.

Important fields:

  • Package: Package name (lowercase, alphanumeric + hyphens)
  • Version: Semantic version (e.g., 1.0.0)
  • Architecture: all (scripts) or amd64, arm64, etc.
  • Section: Category (utils, admin, net, etc.)
  • Description: First line = short description, rest = long description (must be indented)

Place files in the directory structure exactly as they should appear on the target system:

<package-name>/usr/local/bin/my-script

Will be installed to:

/usr/local/bin/my-script
  • Control files: 644
  • Executable scripts: 755
  • Configuration files: 644
Terminal window
./build.sh <package-name>
sudo dpkg -i <package-name>_<version>_<arch>.deb

Simple utility script demonstrating:

  • Basic bash script packaging
  • System information display
  • Installation verification

Files:

  • hello-toolsera/DEBIAN/control - Package metadata
  • hello-toolsera/usr/local/bin/hello-toolsera - Executable script

Build: ./build.sh hello-toolsera Install: sudo apt install hello-toolsera Run: hello-toolsera

A packaged systemd service, and the reference for anything more involved than a script on PATH. Demonstrates:

  • Sourcing the payload from utils/qbittorrent-automation/ rather than copying it into packages/, so the checkout install and the .deb cannot drift apart
  • Maintainer scripts via scripts: — a dedicated system user, a venv for a PyPI-only dependency, and conditional enable/start
  • A conffile at /etc/qbt-tracker-remover/env holding the secrets

Files:

  • qbt-tracker-remover/package.yml — manifest (mode: fpm)
  • scripts/qbt-tracker-remover/qbt-tracker-remover/usr/bin runner; picks BWS or plain-env mode from the environment, and carries the configure subcommand
  • scripts/qbt-tracker-remover/qbt-tracker-remover.service — the packaged unit
  • scripts/qbt-tracker-remover/env.default — the shipped conffile
  • scripts/qbt-tracker-remover/{postinst,prerm,postrm} — maintainer scripts

Build: ./build.sh qbt-tracker-remover Install: sudo apt install qbt-tracker-remover

qbittorrent-api is not in the Debian archive, so postinst builds a venv at /usr/lib/qbt-tracker-remover/venv from PyPI. Installing therefore needs outbound network access, and a failed pip fails the install deliberately rather than leaving a service that cannot start.

  1. Versioning: Use semantic versioning (MAJOR.MINOR.PATCH)
  2. Testing: Always test locally before uploading to Nexus
  3. Documentation: Include usage information in package description
  4. Dependencies: Declare dependencies in control file with Depends: field
  5. Conflicts: Declare conflicts with Conflicts: field
  6. Post-install scripts: Use postinst, prerm, postrm scripts in DEBIAN/ if needed
  7. Idempotency: Ensure packages can be reinstalled without issues
Terminal window
# Check package structure
dpkg-deb --contents package.deb
# Check for errors
dpkg-deb --info package.deb
  1. Run Rebuild APT repository metadata task in Nexus UI
  2. Check Nexus logs for errors
  3. Verify GPG signing key is configured
Terminal window
# Verify repository is configured
cat /etc/apt/sources.list.d/toolsera.list
# Update cache
sudo apt update
# Search for package
apt-cache search hello-toolsera