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/.
Directory Structure
Section titled “Directory Structure”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 scriptsBuilding Packages
Section titled “Building Packages”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.
Prerequisites
Section titled “Prerequisites”dpkg-deb(usually pre-installed on Debian/Ubuntu)- Bash shell
Build Process
Section titled “Build Process”-
Navigate to packages directory:
Terminal window cd packages/ -
Make build script executable (first time only):
Terminal window chmod +x build.sh -
Build a package:
Terminal window ./build.sh <package-name>Example:
Terminal window ./build.sh hello-toolsera -
Output: The
.debfile will be created in the current directory:hello-toolsera_1.0.0_all.deb
Testing Packages Locally
Section titled “Testing Packages Locally”Before uploading to Nexus, test the package locally:
# Inspect package metadatadpkg-deb --info hello-toolsera_1.0.0_all.deb
# View package contentsdpkg-deb --contents hello-toolsera_1.0.0_all.deb
# Install packagesudo dpkg -i hello-toolsera_1.0.0_all.deb
# Test the installed commandhello-toolsera
# Remove package (if needed)sudo apt remove hello-toolseraReleasing via CI (recommended)
Section titled “Releasing via CI (recommended)”Every package is described by packages/debian-ubuntu/<pkg>/package.yml (see existing
packages for examples). To ship a new version:
- Bump the
version:field inpackage.yml(and theVersion:line inDEBIAN/controlformode: dpkgpackages). - Commit and merge to
main. - Push a tag of the form
pkg/<package>-<version>, e.g.:Terminal window git tag pkg/hello-toolsera-1.0.1git 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).
File permissions in mode: fpm manifests
Section titled “File permissions in mode: fpm manifests”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:
0755for anything landing in/bin,/sbin,/usr/bin,/usr/sbin,/usr/local/bin,/usr/local/sbin0644for everything else; directories are always0755
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/envbefore_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.
Uploading Manually (fallback)
Section titled “Uploading Manually (fallback)”When CI is unavailable, the same scripts the workflow uses work locally:
cd packages/debian-ubuntu./build.sh hello-toolsera --expected-version 1.0.1
export NEXUS_URL=https://nexus.toolsera.lanexport NEXUS_REPO=toolseraexport NEXUS_USER=adminexport NEXUS_PASS='...'./upload.sh hello-toolsera_1.0.1_amd64.debOr fetch the credentials from BWS the same way CI does:
bws run --access-token "$BWS_ACCESS_TOKEN" -- ./upload.sh hello-toolsera_1.0.1_amd64.debThe Nexus Web UI (Browse → Repositories → toolsera → Upload component) is also still available for one-off uploads.
Using Packages from Repository
Section titled “Using Packages from Repository”Once uploaded and metadata is rebuilt, install packages on client machines:
# Update package listsudo apt update
# Install packagesudo apt install hello-toolsera
# Run commandhello-toolseraCreating New Packages
Section titled “Creating New Packages”1. Create Package Directory Structure
Section titled “1. Create Package Directory Structure”mkdir -p <package-name>/DEBIANmkdir -p <package-name>/usr/local/bin # Or other paths as needed2. Create DEBIAN/control File
Section titled “2. Create DEBIAN/control File”Package: <package-name>Version: 1.0.0Section: utilsPriority: optionalArchitecture: allMaintainer: 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) oramd64,arm64, etc.Section: Category (utils, admin, net, etc.)Description: First line = short description, rest = long description (must be indented)
3. Add Package Files
Section titled “3. Add Package Files”Place files in the directory structure exactly as they should appear on the target system:
<package-name>/usr/local/bin/my-scriptWill be installed to:
/usr/local/bin/my-script4. Set Permissions (in build script)
Section titled “4. Set Permissions (in build script)”- Control files:
644 - Executable scripts:
755 - Configuration files:
644
5. Build and Test
Section titled “5. Build and Test”./build.sh <package-name>sudo dpkg -i <package-name>_<version>_<arch>.debExample Packages
Section titled “Example Packages”hello-toolsera
Section titled “hello-toolsera”Simple utility script demonstrating:
- Basic bash script packaging
- System information display
- Installation verification
Files:
hello-toolsera/DEBIAN/control- Package metadatahello-toolsera/usr/local/bin/hello-toolsera- Executable script
Build: ./build.sh hello-toolsera Install: sudo apt install hello-toolsera
Run: hello-toolsera
qbt-tracker-remover
Section titled “qbt-tracker-remover”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 intopackages/, so the checkout install and the.debcannot 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/envholding the secrets
Files:
qbt-tracker-remover/package.yml— manifest (mode: fpm)scripts/qbt-tracker-remover/qbt-tracker-remover—/usr/binrunner; picks BWS or plain-env mode from the environment, and carries theconfiguresubcommandscripts/qbt-tracker-remover/qbt-tracker-remover.service— the packaged unitscripts/qbt-tracker-remover/env.default— the shipped conffilescripts/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.
Best Practices
Section titled “Best Practices”- Versioning: Use semantic versioning (MAJOR.MINOR.PATCH)
- Testing: Always test locally before uploading to Nexus
- Documentation: Include usage information in package description
- Dependencies: Declare dependencies in
controlfile withDepends:field - Conflicts: Declare conflicts with
Conflicts:field - Post-install scripts: Use
postinst,prerm,postrmscripts inDEBIAN/if needed - Idempotency: Ensure packages can be reinstalled without issues
Troubleshooting
Section titled “Troubleshooting”Package won’t install
Section titled “Package won’t install”# Check package structuredpkg-deb --contents package.deb
# Check for errorsdpkg-deb --info package.debNexus repository not updating
Section titled “Nexus repository not updating”- Run Rebuild APT repository metadata task in Nexus UI
- Check Nexus logs for errors
- Verify GPG signing key is configured
APT can’t find package
Section titled “APT can’t find package”# Verify repository is configuredcat /etc/apt/sources.list.d/toolsera.list
# Update cachesudo apt update
# Search for packageapt-cache search hello-toolseraResources
Section titled “Resources”Related Files
Section titled “Related Files”- Build script reference:
debian-ubuntu/BUILD-SCRIPT.md - FPM packaging guide:
debian-ubuntu/FPM-PACKAGING-GUIDE.md - Ansible role for APT repository setup:
ansible/playbooks/roles/custom_apt_repo/ - Setup new machine playbook:
ansible/playbooks/setup_new_machine.yml