Debian packaging via per-component Makefiles + fpm#347
Merged
Conversation
228b852 to
3f506d4
Compare
This was
linked to
issues
Jun 15, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a packaging-and-release pipeline that builds the three deployable components as OS packages (via per-component Makefiles + fpm), publishes them to GitHub Releases as a flat APT repository, and updates the Docker images to install those packages instead of copying/building the repo in-image.
Changes:
- Added per-component packaging Makefiles (+ fpm metadata) and a shared
package-versionhelper derived fromgit describe. - Added a
builderimage and Docker Bake wiring so docs/agent/manager images install.debartifacts produced by the builder stage. - Added/updated CI + developer docs to publish release assets (debs +
Packages/Packages.gz) and document the release pipeline.
Reviewed changes
Copilot reviewed 31 out of 34 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
pull-config/Makefile |
New component Makefile: stages pull-config + instances + error pages and builds packages via fpm. |
pull-config/install.sh |
Removes the legacy imperative install script (replaced by Makefile install). |
pull-config/.gitignore |
Ignores component packaging artifacts (.pkg/, built packages). |
pull-config/.fpm |
Defines opensource-agent package metadata and Debian dependencies. |
package-version |
New helper to derive format-specific package versions from git tags/describe output. |
mie-opensource-landing/zensical.toml |
Adds “Release Pipeline” to docs navigation. |
mie-opensource-landing/Makefile |
New component Makefile to build/stage docs site and package with fpm. |
mie-opensource-landing/docs/developers/release-pipeline.md |
New developer documentation describing packaging, images, and release workflow. |
mie-opensource-landing/docs/developers/pull-config.md |
Updates pull-config docs to reflect Makefile-based build/packaging. |
mie-opensource-landing/docs/developers/docker-images.md |
Updates image docs to describe installing packaged payloads via builder context. |
mie-opensource-landing/.gitignore |
Ignores docs packaging artifacts and built packages. |
mie-opensource-landing/.fpm |
Defines opensource-docs package metadata. |
Makefile |
Replaces old install targets with delegation to component Makefiles and adds package collection to dist/. |
LICENSE |
Adds Apache-2.0 license text. |
images/opensource-server.sources |
Adds an APT source file pointing at releases/latest/download/ (trusted). |
images/manager/Dockerfile |
Installs packaged manager/docs artifacts from builder output; enables init unit; keeps PG install. |
images/manager/container-creator-init.service |
Adjusts init sequencing and runs sequelize migrations + seeds on first boot. |
images/docs/Dockerfile |
Installs nginx + opensource-docs from builder output; stages the APT source file. |
images/docker-bake.hcl |
Adds a builder target and wires it as a context for docs/agent/manager builds. |
images/builder/Dockerfile |
New artifact-only image that builds the three .deb packages into /dist. |
images/agent/Dockerfile |
Installs opensource-agent package from builder output; stages the APT source file. |
create-a-container/Makefile |
New component Makefile: builds client bundle, stages runtime tree + units + logrotate, packages with fpm. |
create-a-container/contrib/systemd/job-runner.service |
Adds packaged job-runner systemd unit. |
create-a-container/contrib/systemd/container-creator.service |
Fixes log directory creation by using LogsDirectory. |
create-a-container/contrib/preremove.sh |
Adds pre-removal script to stop/disable units on package removal. |
create-a-container/contrib/postinstall.sh |
Adds post-install script to enable and (when applicable) restart units. |
create-a-container/contrib/opensource-server.logrotate |
Adds packaged logrotate config for /var/log/opensource-server/*.log. |
create-a-container/.gitignore |
Ignores component packaging artifacts and built packages. |
create-a-container/.fpm |
Defines opensource-server package metadata, deps, scripts, and conffile. |
compose.yml |
Tweaks dev install commands for create-a-container (server deps omit dev; client deps installed explicitly). |
.gitignore |
Ignores top-level dist/ artifacts. |
.github/workflows/release.yml |
New workflow: builds packages via bake, generates Packages/Packages.gz, uploads to the release. |
.github/workflows/build-images.yml |
Updates triggers and “latest” tagging behavior to align with non-prerelease releases. |
.dockerignore |
Updates ignored build outputs; intends to ignore packaging artifacts. |
- container-creator-init: invoke node_modules/.bin/sequelize directly instead of 'npm run db:migrate' so npm is not a runtime dependency. - container-creator: create /var/log/opensource-server on demand via systemd LogDirectory instead of shipping an empty directory in the package.
Self-contained Makefile with the standard deps/build/install/dev contract (default goal: build), DESTDIR/PREFIX/VERSION parameters, and deb/rpm/apk targets that stage the component and package it with nfpm. - dev runs 'npm run dev' (server + client watch); dev-client runs only the client bundle watcher for the compose client service. - install stages the app under PREFIX/create-a-container (cp -a to keep node_modules/.bin symlinks), the three systemd units, and the logrotate drop-in. - nfpm.yaml builds the opensource-server package: depends on opensource-agent + opensource-docs (the manager nginx config references their files) plus nodejs/sudo/libc; postinstall enables the units, preremove disables them on real removal. Logrotate ships as a config file. Relocated from images/manager/.
Makefile with deps (uv sync) / build (zensical build, default) / install (stage the prebuilt site under PREFIX) / dev (zensical serve) and deb/rpm/apk targets. nfpm.yaml builds the content-only opensource-docs package (arch all), suggesting nginx.
Makefile with no-op deps/build/dev (plain bash) and an install target that stages the pull-config engine, instances, cron schedule, the shared error pages and the nginx forward-auth cache dir. nfpm.yaml builds the opensource-agent package, depending on nginx (with stream and ModSecurity modules), dnsmasq, cron and curl; instances and cron ship as config files (instances mode 0755).
The root Makefile now delegates the standard deps/build/install/dev/ deb/rpm/apk contract to the three component Makefiles and forwards PREFIX/DESTDIR/VERSION. 'make deb' builds all three packages and collects them into ./dist. Default goal is build.
Add images/builder, a Debian image that runs 'make deb' (Node + uv + nfpm) to produce the three packages, exported as an artifact-only stage. docs/agent/manager now install those packages via a builder context instead of copying the repo and running make: - agent: installs opensource-agent; keeps image-only conffile edits (ModSecurity, dnsmasq, acme.sh). - docs: installs opensource-docs + nginx; keeps the vhost. - manager: installs opensource-docs + opensource-server (agent already present); keeps PGDG postgres + the Proxmox snakeoil drop-in. Each leaf image stages /etc/apt/sources.list.d/opensource-server.sources (flat repo at releases/latest/download) so 'apt upgrade' tracks future releases. The source is added last / removed-then-re-added in manager so an 'apt update' during the build is never blocked by the release not existing yet. Also fix container-creator.service to use the correct systemd directive LogsDirectory (not LogDirectory), so /var/log/opensource-server is created on demand; the typo silently disabled it and the app crashed opening access.log.
The node, client and zensical dev services now install make and invoke the component Makefile targets instead of inlining npm/uv commands: - node: 'make deps' (create-a-container) - client: 'make dev-client' (client bundle watcher only; the server runs inside Proxmox) - zensical: 'make dev' (docs live server), preserving VIRTUAL_ENV Keeps the dev workflow in lockstep with the packaging build steps.
- release.yml: on a v* tag, build the three packages via the builder bake target, generate flat APT repo metadata (Packages, Packages.gz) with dpkg-scanpackages, and attach the debs + metadata to the GitHub release so releases/latest/download serves a working 'deb [trusted=yes] ... ./' source. - build-images.yml: expand the paths filters to the component directories, **/Makefile, **/nfpm.yaml and .dockerignore, since the images now build from the component packages.
Add a release trigger (published, released) and gate the :latest tag on a non-prerelease release event instead of every push to main. Merges to main still publish branch/sha tags; :latest now moves only when a real (non-prerelease) GitHub release is published, keeping the :latest image channel aligned with the releases/latest deb channel.
- Add developers/release-pipeline.md covering the component Makefile contract (deps/build/install/dev), nfpm packaging and the three package names, the builder image, the flat APT repo on GitHub Releases, the deb apt source, and the latest-on-non-prerelease rule. - Wire it into the docs nav. - docker-images.md: fix the stale lego reference (acme.sh), describe the package-based image composition and the builder target. - pull-config.md: note the Makefile/package supersede install.sh. - release.yml: also publish *_latest.deb stable-name aliases.
- nfpm.yaml no longer fights 'make install': /etc files are declared explicitly as config and the rest is packaged per top-level directory (/opt, /usr), so no file appears in both a tree and a config entry. 'make package' no longer rm -rf's or mutates the staged build-root; it reuses the DESTDIR install (run 'make clean' first for a pristine build). - container-creator-init.service is the manager image's concern, not the package's: it provisions a *local* PostgreSQL, which the package only suggests. Moved the unit to images/manager and made it Requires=postgresql.service. The package now ships only container-creator + job-runner; postinstall/preremove manage those two.
Apply the same nfpm tree-split as opensource-server: /etc files are declared explicitly as config entries and the rest is packaged per top-level directory, so no file appears in both a tree and a config entry. 'make package' now reuses the DESTDIR install instead of rm -rf'ing the staged tree and deleting conffiles from it (run 'make clean' first for a pristine package).
- Bind-mount the builder's /dist with RUN --mount=from=builder instead
of COPY + RUN, removing the extra image layer per package install.
- Stage the release APT source once and leave it in place for the whole
build (and the final image) instead of removing and re-adding it. A
missing release is a non-fatal 'apt update' warning for our own
install steps ('|| true'); the PGDG setup script does a strict update,
so the source is moved aside and restored within that single layer.
- Install and enable container-creator-init.service in the manager image
(it provisions local PostgreSQL, an image concern, not the package's).
Installing make in each slim dev image was ugly. The node, client and zensical services emulate their component make targets (deps, dev-client, dev) directly with npm/uv instead, matching the original approach. Comments cross-reference the equivalent make target.
Reflect that container-creator-init lives in the manager image (not the package), the nfpm tree-split that avoids the config/tree collision and the no-rm-rf packaging, and the RUN --mount install with the apt source left in place.
…ild-root - The pull-config engine and instances are program code, not configuration (runtime config comes from /etc/environment, which is not packaged), so nothing is tagged as a config file. - nfpm.yaml no longer hard-codes ./build-root: nfpm runs from the staging root, so content sources are relative (etc, opt, var) and work for any DESTDIR. Packaging stages into a dedicated .pkg-root (never a user-supplied DESTDIR); make clean removes only that staging dir.
nfpm now runs from a dedicated .pkg-root staging dir, so nfpm.yaml content sources are relative (opt, usr, etc) and support any DESTDIR without hard-coding build-root. Removed the install-buildroot helper target. For opensource-server, maintainer scripts are staged under .pkg-root/.nfpm/ (outside the packaged contents) so they resolve relative to nfpm's working directory. make clean only removes the internal staging dir, never a user-supplied DESTDIR.
Use nfpm's expand:true on content entries to interpolate ${DESTDIR} in
the source paths, so nfpm.yaml no longer hard-codes the staging path and
nfpm runs from the component directory (not the staging root). Rename the
dedicated packaging staging dir to .nfpm/buildroot, descriptive of the
tooling.
…rib/
Group the distro-integration files under contrib/ (the conventional
location): contrib/systemd/{container-creator,job-runner}.service,
contrib/{postinstall,preremove}.sh and contrib/opensource-server.logrotate.
Update the Makefile install paths and nfpm.yaml script references
accordingly. Also adopt expand:true for ${DESTDIR} interpolation and the
.nfpm/buildroot staging dir, and drop the '|| true' guards from the
maintainer scripts in favor of detecting systemctl availability and
whether systemd is running.
…ease Drop the '|| true' guards and the manager's source move-aside dance. Now that every release carries flat APT repository metadata (an empty Packages index suffices for the bootstrap release), apt-get update against the release source succeeds throughout the image build, so the release source can stay in place and failures are surfaced rather than ignored. Also drop the redundant '|| true' on systemctl enable, which succeeds at build time by creating static symlinks.
Each component's clean target now also removes its built *.deb/*.rpm/*.apk artifacts, and the top-level clean removes the dist/ collection directory in addition to delegating to the components.
The CI image build failed with 'version number does not start with digit' because a shallow checkout has no tags, so 'git describe --tags --always' returned a bare commit SHA which is not a valid Debian version. - VERSION now falls back to 0.0.0+g<sha> when no tag is reachable, and only uses the tag-derived form when 'git describe --tags' succeeds, so the version is always digit-leading and valid. - build-images.yml checks out with fetch-depth: 0 so the builder image's git describe sees tags and produces the proper 2026.6.2+N.gSHA version (release.yml already did this).
The release workflow no longer creates or mutates the GitHub release. It runs on 'release: published' (full or prerelease), checks out the release tag, builds the packages, and uploads the debs + flat-repo metadata to the triggering release via 'gh release upload --clobber'. This removes the prerelease race entirely: create the release (choosing full vs prerelease) in GitHub first, then the action just attaches assets. workflow_dispatch still builds packages for a manual test but skips the upload. Docs updated accordingly.
The previous version logic translated prerelease suffixes to deb's ~rc1 form, which is invalid for apk (which needs _rc1). Instead, VERSION now produces a plain semver string (e.g. 2026.6.3-rc1, 2026.6.3-rc1+15.gSHA) and nfpm's default semver schema renders the correct version for each packager: ~rc1 for deb/rpm, _rc1 for apk, with snapshot metadata mapped to each format's convention. Verified prerelease and snapshot builds across deb, rpm and apk.
Tags are now unprefixed semver (e.g. 2026.6.3, 2026.6.3-rc1), so the '^v' strip is removed from the VERSION sed in all three component Makefiles. git describe must reach an unprefixed tag for a valid version.
Now that releases are tagged, git describe always finds a tag, so the VERSION sed collapses to a single describe|sed pipeline.
metadata-action's flavor latest=auto adds :latest for type=ref,event=tag events independently of the explicit type=raw,value=latest rule, so publishing the 2026.6.3 prerelease moved :latest even though the raw rule was disabled for prereleases. Set flavor: latest=false on every metadata step so :latest is controlled solely by the explicit non-prerelease-gated raw rule.
Each Makefile now parses VERSION (leading v stripped, 0.0.0 fallback), PRERELEASE, commit count, hash, and dirty state from git describe --tags --long --dirty. scripts/nfpm-version.sh composes a format-appropriate version string (deb: ~pre +meta; rpm: ~pre ^snap; apk: _pre _gitN) and nfpm.yaml sets version_schema: none so nfpm uses it verbatim instead of its own semver parsing.
Move all version parsing and per-format composition into a single script at the repo root, ./package-version <deb|rpm|apk>, which derives the version from git itself. The component Makefiles no longer parse git or hold version parts; their package targets just call the script. The script has no .sh suffix and no tool-specific name so the implementation can change later. Drop the now-unused VERSION plumbing from the top-level Makefile.
Replace the three nfpm.yaml files with fpm options files
(<package>.fpm) holding the static package metadata, and have each
Makefile's package target invoke fpm with the dynamic options on the
CLI (output type, version from ../package-version, arch, staging dir,
and the manager's version-pinned inter-package deps).
fpm's dir input packages the staged tree verbatim from -C <stage>,
preserving symlinks (e.g. node_modules/.bin/sequelize) and the
directory layout, which removes nfpm's awkward per-directory src/dst
'tree' entries and ${DESTDIR} interpolation. Config files are marked
explicitly with --config-files and --deb-no-default-config-files stops
the auto-marking of everything under /etc that nfpm/dpkg defaults to.
The packaging staging dir is renamed from .nfpm/buildroot to
.pkg/buildroot (a dir literally named .fpm is consumed by fpm as an rc
options file).
Rename the per-component fpm options files to .fpm so fpm auto-loads them, dropping --fpm-options-file. Move --architecture into each .fpm. Let fpm copy the whole staging root from -C instead of listing the top-level dirs. Add opensource-agent/opensource-docs to the manager's .fpm as plain (unversioned) depends, dropping the per-format version-pinned --depends from the Makefile.
Replace the nfpm .deb install with ruby + fpm via gem.
Update release-pipeline.md to describe the fpm-based packaging: .fpm options files, the package-version composer, fpm's dir input from the staging root, and explicit config-file marking. Correct the note that pull-config instances are program code, not config files.
Add a help target listing the available targets and variables to the top-level and each component Makefile, and set it as .DEFAULT_GOAL so a bare 'make' prints usage instead of building. Also drop the stale nfpm references from the create-a-container header comment.
The Makefiles stage into .pkg/buildroot, not build-root, so the stale **/build-root pattern let local packaging artifacts leak into the build context.
Restrict the push trigger to main so feature-branch pushes don't build images (their PR covers that), reducing CI/registry churn. No paths filter is added, so doc-only changes still rebuild the images.
Correct the Makefile contract table (default goal is help, add the help target, drop the VERSION override row), describe ./package-version as the version source, and state consistently that a leading v on the tag is optional.
ebda76f to
67b06c9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces Debian packaging for the three deployable components using per-component Makefiles + fpm publishes them to GitHub Releases as a flat APT repository, and rebuilds the container images to install those packages. The same component build commands are reused by local development (compose), the images, and CI.
Packages
create-a-container/opensource-servermie-opensource-landing/opensource-docspull-config/opensource-agentEverything installs under the
/opt/opensource-serverprefix (matching existing runtime references).opensource-serverdepends onopensource-agent+opensource-docs(the manager's rendered nginx config serves their files).What's included
Makefiles with a uniform contract —deps,build(default goal),install(DESTDIR/PREFIX-aware),dev, anddeb/rpm/apk.VERSIONis derived from git tags inline. The top-levelMakefiledelegates to all three and collects packages intodist/.make cleanremoves staging, build outputs, and built packages.nfpm.yamlper component — packages the staged tree via${DESTDIR}interpolation (expand: true), so the config stays DRY and supports anyDESTDIR.opensource-serverships its systemd units and enables them via apostinstallscript; the logrotate drop-in is a config file. Distro-integration files live undercreate-a-container/contrib/.images/builder— runsmake deb(Node.js, uv, nfpm) to produce the packages as an artifact-only stage. Thedocs/agent/managerimages install them viaRUN --mount=from=builder(no extra layers) and stage the release APT source soapt upgradetracks future releases.release.yml(onv*tags) builds the packages, generates flat-repo metadata (Packages,Packages.gz) and*_latest.debaliases, and attaches them to the release.build-images.ymlnow tags:latestonly on non-prerelease releases.developers/release-pipeline.mdplus stale-doc refreshes.LICENSE— Apache-2.0.Behavioral changes worth noting
container-creator-init.service(provisions a local PostgreSQL) moved from the package to the manager image, since the package only suggests postgresql and works with a remote database too. The package shipscontainer-creator+job-runner.LogsDirectorydirective (was the invalidLogDirectory, which systemd silently ignored).:latestimages now track non-prerelease releases instead of every merge tomain.Install / update (admins)
Migrating an existing (non-package) system to packages
Existing deployments run from an in-place tree at
/opt/opensource-server(a git checkout built with the old
make install), with systemd units in/etc/systemd/system/. The packages own files under/opt/opensource-server/<component>/and ship units in
/usr/lib/systemd/system/, so the old in-place files must beremoved first or
dpkgwill refuse to overwrite files it does not own.The good news: the package layout uses the same prefix and the same config
paths, so stateful config is preserved across the switch:
/etc/default/container-creator(manager DB credentials) — kept; the packagedcontainer-creator/job-runnerunits read the same file, so the existingdatabase keeps working (local or remote).
/etc/pull-config.d/*and/etc/cron.d/pull-config(agent) — these areconffiles in the package; local customizations are preserved on install.
/var/log/opensource-server— recreated on demand by the unit'sLogsDirectory.Steps (run on the target container/host)
Notes:
(
container-creator-init.service) is part of the image, not the package. Ona bare-metal/LXC host that already has
/etc/default/container-creator, it isnot needed — the existing DB config is reused. For a brand-new manager without
that file, provision the database and write
/etc/default/container-creatoryourself (local or remote), then start
container-creator.apt update && apt upgrade(the source tracks thelatest non-prerelease release).
dpkgstill reports a conflict on a specific file under/opt/opensource-server, a leftover from the old tree was missed — remove thatpath and re-run
apt install.Verification
make deb; inspected metadata, deps, conffiles, scripts, modes, and thenode_modules/.bin/sequelizesymlink.:80, agent has pull-config + cron, manager has all units active (incl. the image-owned init), app on:3000, DB provisioned,/var/log/opensource-serverauto-created, andapt updateworks inside the final image.:main), seeded DB state (a marker row; 53 applied migrations), then ran the migration steps above against the2026.6.3prerelease (via its tag URL). Result:apt install opensource-serversucceeded with nodpkgfile conflicts, units moved to/usr/lib/systemd/systemand were enabled + restarted by the postinstall, the app returned200, and all DB state and/etc/default/container-creatorcredentials were preserved (marker row intact, migration count unchanged, password identical). Agent (pull-config/cron/nginx/dnsmasq), docs site, and error-pages were all functional afterward.Notes / follow-ups
v2026.6.2release needed a manualPackages.gzupload (GitHub rejects the empty uncompressedPackages; apt uses the.gz). Future releases generate both automatically.Trusted: yesfor now).Draft for review — happy to split into smaller PRs if preferred.