From 0b34b182a9deeb9e141f8113e72fe347401f2aa0 Mon Sep 17 00:00:00 2001 From: Hein Date: Wed, 8 Apr 2026 14:28:09 +0200 Subject: [PATCH] feat(release): add packaging support for Arch, Debian, and RPM * Implement Arch package build process in release workflow * Add Debian and RPM packaging specifications * Include man page installation in Makefile * Create unitdore man page for documentation --- .gitea/workflows/release.yml | 165 +++++++++++++++++++++++++++++++++++ Makefile | 6 +- docs/unitdore.1 | 143 ++++++++++++++++++++++++++++++ pkg/arch/PKGBUILD | 44 ++++++++++ pkg/centos/unitdore.spec | 45 ++++++++++ pkg/debian/control | 10 +++ 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 docs/unitdore.1 create mode 100644 pkg/arch/PKGBUILD create mode 100644 pkg/centos/unitdore.spec create mode 100644 pkg/debian/control diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 7d3f754..17a5f17 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -91,3 +91,168 @@ jobs: done env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pkg-arch: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build Arch package + run: | + VERSION="${{ github.event.inputs.tag || github.ref_name }}" + PKGVER="${VERSION#v}" + + # Source tarball — prefix=unitdore/ matches `cd "$pkgname"` in PKGBUILD + git archive --format=tar.gz --prefix=unitdore/ HEAD \ + > pkg/arch/unitdore-${PKGVER}.tar.gz + SHA=$(sha256sum pkg/arch/unitdore-${PKGVER}.tar.gz | cut -d' ' -f1) + + # Patch PKGBUILD for local build + sed -i \ + -e "s/^pkgver=.*/pkgver=${PKGVER}/" \ + -e "s/^sha256sums=.*/sha256sums=('${SHA}')/" \ + -e "s|source=.*|source=(\"unitdore-\${pkgver}.tar.gz\")|" \ + pkg/arch/PKGBUILD + + mkdir -p pkg/arch/out + docker run --rm \ + -v "$PWD/pkg/arch:/build" \ + -v "$PWD/pkg/arch/out:/out" \ + -w /build \ + archlinux:latest \ + bash -c " + pacman -Syu --noconfirm base-devel go && + useradd -m builder && + chown -R builder:builder /build && + runuser -u builder -- makepkg --noconfirm --noprogressbar && + cp /build/*.pkg.tar.zst /out/ + " + + - name: Upload to release + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" + RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \ + -H "Authorization: token ${GITHUB_TOKEN}") + UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4) + for f in pkg/arch/out/*.pkg.tar.zst; do + FNAME=$(basename "$f") + echo "Uploading $FNAME..." + curl -s -X POST "${UPLOAD_URL}?name=${FNAME}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${f}" > /dev/null + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pkg-deb: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build Debian packages + run: | + VERSION="${{ github.event.inputs.tag || github.ref_name }}" + PKGVER="${VERSION#v}" + + for GOARCH in amd64 arm64; do + GOOS=linux GOARCH=$GOARCH go build \ + -trimpath \ + -ldflags "-X main.version=${PKGVER}" \ + -o unitdore . + + PKGDIR="unitdore_${PKGVER}_${GOARCH}" + mkdir -p "${PKGDIR}/DEBIAN" + mkdir -p "${PKGDIR}/usr/bin" + mkdir -p "${PKGDIR}/usr/share/man/man1" + mkdir -p "${PKGDIR}/etc/unitdore" + + install -m755 unitdore "${PKGDIR}/usr/bin/unitdore" + install -m644 docs/unitdore.1 "${PKGDIR}/usr/share/man/man1/unitdore.1" + + sed -e "s/VERSION/${PKGVER}/" \ + -e "s/ARCH/${GOARCH}/" \ + pkg/debian/control > "${PKGDIR}/DEBIAN/control" + + dpkg-deb --build --root-owner-group "${PKGDIR}" + echo "Built ${PKGDIR}.deb" + done + + - name: Upload to release + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" + RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \ + -H "Authorization: token ${GITHUB_TOKEN}") + UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4) + for f in *.deb; do + FNAME=$(basename "$f") + echo "Uploading $FNAME..." + curl -s -X POST "${UPLOAD_URL}?name=${FNAME}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${f}" > /dev/null + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + pkg-rpm: + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build RPM + run: | + VERSION="${{ github.event.inputs.tag || github.ref_name }}" + PKGVER="${VERSION#v}" + + # Source tarball — prefix=unitdore-VERSION/ matches RPM %autosetup convention + git archive --format=tar.gz --prefix=unitdore-${PKGVER}/ HEAD \ + > unitdore-${PKGVER}.tar.gz + + # Patch spec version + sed -i "s/^Version:.*/Version: ${PKGVER}/" pkg/centos/unitdore.spec + + mkdir -p pkg/centos/out + docker run --rm \ + -v "$PWD:/workspace" \ + -v "$PWD/pkg/centos/out:/out" \ + -w /workspace \ + rockylinux:9 \ + bash -c " + dnf install -y rpm-build golang git && + mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} && + cp unitdore-${PKGVER}.tar.gz ~/rpmbuild/SOURCES/ && + cp pkg/centos/unitdore.spec ~/rpmbuild/SPECS/ && + rpmbuild -ba ~/rpmbuild/SPECS/unitdore.spec && + find ~/rpmbuild/RPMS -name '*.rpm' -exec cp {} /out/ \; + " + + - name: Upload to release + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" + RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \ + -H "Authorization: token ${GITHUB_TOKEN}") + UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4) + for f in pkg/centos/out/*.rpm; do + FNAME=$(basename "$f") + echo "Uploading $FNAME..." + curl -s -X POST "${UPLOAD_URL}?name=${FNAME}" \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${f}" > /dev/null + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index a3187d1..5465592 100644 --- a/Makefile +++ b/Makefile @@ -12,16 +12,18 @@ all: build build: go build $(LDFLAGS) -o $(BINARY) . -## install: install binary and create config dir +## install: install binary, man page, and create config dir install: build install -Dm755 $(BINARY) $(INSTALL_DIR)/$(BINARY) + install -Dm644 docs/unitdore.1 /usr/share/man/man1/unitdore.1 mkdir -p $(CONFIG_DIR) @echo "Installed to $(INSTALL_DIR)/$(BINARY)" @echo "Config dir: $(CONFIG_DIR)" -## uninstall: remove binary +## uninstall: remove binary and man page uninstall: rm -f $(INSTALL_DIR)/$(BINARY) + rm -f /usr/share/man/man1/unitdore.1 @echo "Removed $(INSTALL_DIR)/$(BINARY)" ## test: run all unit tests diff --git a/docs/unitdore.1 b/docs/unitdore.1 new file mode 100644 index 0000000..474b742 --- /dev/null +++ b/docs/unitdore.1 @@ -0,0 +1,143 @@ +.TH UNITDORE 1 "2026-04-08" "0.1.0" "User Commands" +.SH NAME +unitdore \- manage container units via systemd +.SH SYNOPSIS +.B unitdore +[\fB\-\-config\fR \fIpath\fR] +\fIcommand\fR +[\fIarguments\fR] +.SH DESCRIPTION +.B unitdore +bridges your container runtime (Podman, Docker) and systemd. +It discovers running containers, stores them in a config file, and generates +and manages systemd \fI.service\fR units for each one. +.SH OPTIONS +.TP +.BR \-\-config " " \fIpath\fR +Path to the units config file. Default: \fI/etc/unitdore/units.yaml\fR +.SH COMMANDS +.TP +.B syncup +Query all available runtimes for running containers and add any new ones to +the config. Reconciles existing entries: if a configured unit's container no +longer exists it is marked disabled with a reason. +.TP +.B install +Generate \fI.service\fR files for all enabled units and write them to the +appropriate systemd directory. Use \fB\-\-dry\-run\fR to preview without writing. +.TP +.B uninstall \fIname\fR +Disable, stop, and remove the service file for a specific unit. +.TP +.B startall +Run \fBsystemctl enable \-\-now\fR for all enabled, installed units in startup +order. +.TP +.B stopall +Run \fBsystemctl disable \-\-now\fR for all installed units in reverse startup +order. +.TP +.B start \fIname\fR +Start a specific unit by name (\fBsystemctl start\fR). +.TP +.B stop \fIname\fR +Stop a specific unit by name (\fBsystemctl stop\fR). +.TP +.B update \fIname\fR +Update a unit's config and recreate its service file. +.RS +.TP +.BR \-\-order " " \fIn\fR +Set the startup order. +.TP +.B \-\-enable +Enable the unit. +.TP +.B \-\-disable +Disable the unit. +.RE +.TP +.B status +Print a summary table of all managed units showing runtime, user, enabled +state, installed state, systemd active state, and disabled reason. +.TP +.B list +Print a table of all tracked units including the path to each service file. +.TP +.B edit +Open the units config file in \fB$EDITOR\fR. Falls back to \fBnvim\fR, +\fBnano\fR, \fBvi\fR in that order. +.SH CONFIG FILE +.B Location: +\fI/etc/unitdore/units.yaml\fR +.PP +.nf +prefix: "" # prepended to all service names, e.g. "prod-" +suffix: "" # appended to all service names, e.g. "-svc" +service_user: "" # User= in [Service]; defaults to current OS user + +units: + - name: nginx + runtime: podman # podman | docker + user: "" # empty = system unit; set for rootless user unit + command: "" # optional: override ExecStart + order: 1 # startup order (lower = earlier) + delay: 0s # delay after previous order group + enabled: true + disabled_reason: "" # auto-set by syncup reconciliation +.fi +.SH GENERATED SERVICE FILES +System units are written to \fI/etc/systemd/system/\fR and user units to +\fI~/.config/systemd/user/\fR. +Service names follow the pattern: +.PP +.RS +\fBunitdore-\fR[\fIprefix\fR]\fIname\fR[\fIsuffix\fR]\fB.service\fR +.RE +.SH EXAMPLES +Discover containers and install their service files: +.PP +.RS +.nf +sudo unitdore syncup +sudo unitdore install +sudo unitdore startall +.fi +.RE +.PP +Preview generated service files without writing: +.PP +.RS +sudo unitdore install \-\-dry\-run +.RE +.PP +Start a single container unit: +.PP +.RS +sudo unitdore start nginx +.RE +.PP +Change startup order and re\-apply service file: +.PP +.RS +sudo unitdore update nginx \-\-order 2 +.RE +.SH FILES +.TP +.I /etc/unitdore/units.yaml +Default configuration file. +.TP +.I /etc/systemd/system/unitdore-*.service +Generated system unit files. +.TP +.I ~/.config/systemd/user/unitdore-*.service +Generated rootless user unit files. +.SH REQUIREMENTS +systemd; Podman and/or Docker (only installed runtimes are used). +Root is required for system units; the relevant user for rootless units. +.SH SEE ALSO +.BR systemctl (1), +.BR podman (1), +.BR docker (1) +.SH AUTHORS +Hein (Warky Devs) \(em https://warky.dev diff --git a/pkg/arch/PKGBUILD b/pkg/arch/PKGBUILD new file mode 100644 index 0000000..0ccaf1f --- /dev/null +++ b/pkg/arch/PKGBUILD @@ -0,0 +1,44 @@ +# Maintainer: Hein (Warky Devs) +pkgname=unitdore +pkgver=0.1.0 +pkgrel=1 +pkgdesc="A door you open and close for container units — manage containers via systemd" +arch=('x86_64' 'aarch64') +url="https://warky.dev" +license=('MIT') +depends=('systemd') +optdepends=( + 'podman: Podman container runtime support' + 'docker: Docker container runtime support' +) +makedepends=('go') +backup=('etc/unitdore/units.yaml') +source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") +sha256sums=('SKIP') + +build() { + cd "$pkgname" + export CGO_ENABLED=0 + go build \ + -trimpath \ + -ldflags "-X main.version=$pkgver" \ + -o "$pkgname" . +} + +check() { + cd "$pkgname" + go test ./... +} + +package() { + cd "$pkgname" + + # Binary + install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname" + + # Man page + install -Dm644 docs/unitdore.1 "$pkgdir/usr/share/man/man1/unitdore.1" + + # Default config dir + install -dm755 "$pkgdir/etc/unitdore" +} diff --git a/pkg/centos/unitdore.spec b/pkg/centos/unitdore.spec new file mode 100644 index 0000000..33ef76d --- /dev/null +++ b/pkg/centos/unitdore.spec @@ -0,0 +1,45 @@ +Name: unitdore +Version: 0.1.0 +Release: 1%{?dist} +Summary: Manage container units via systemd + +License: MIT +URL: https://warky.dev +Source0: %{name}-%{version}.tar.gz + +BuildRequires: golang >= 1.21 +Requires: systemd + +%description +Unitdore bridges your container runtime (Podman, Docker) and systemd. +It discovers running containers, stores them in a config file, and generates +and manages systemd .service units for each one. + +%prep +%autosetup + +%build +export CGO_ENABLED=0 +go build \ + -trimpath \ + -ldflags "-X main.version=%{version}" \ + -o %{name} . + +%install +# Binary +install -Dm755 %{name} %{buildroot}%{_bindir}/%{name} + +# Man page +install -Dm644 docs/unitdore.1 %{buildroot}%{_mandir}/man1/unitdore.1 + +# Config dir +install -dm755 %{buildroot}%{_sysconfdir}/unitdore + +%files +%{_bindir}/%{name} +%{_mandir}/man1/unitdore.1* +%dir %{_sysconfdir}/unitdore + +%changelog +* Tue Apr 08 2026 Hein (Warky Devs) - 0.1.0-1 +- Initial package diff --git a/pkg/debian/control b/pkg/debian/control new file mode 100644 index 0000000..63779d5 --- /dev/null +++ b/pkg/debian/control @@ -0,0 +1,10 @@ +Package: unitdore +Version: VERSION +Architecture: ARCH +Maintainer: Hein (Warky Devs) +Depends: systemd +Recommends: podman | docker.io +Description: A door you open and close for container units + Unitdore bridges your container runtime (Podman, Docker) and systemd. + It discovers running containers, stores them in a config file, and generates + and manages systemd .service units for each one.