commit 2e971845d80d85b04aa135fb7cee8465cc0ec815 Author: Zoran Peričić Date: Tue Aug 18 12:12:45 2026 +0200 Initial semaphore 2.19.8 package with service, container and postgresql subpackages Semaphore UI packaged from the upstream community (MIT) release binaries, following the gitea packaging pattern: native systemd service and Podman quadlet variants (mutually exclusive), with an optional PostgreSQL backend via quadlet drop-ins. SQLite is the default database; session/encryption secrets are generated into /etc/semaphore/env on first install. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3897fbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Downloaded upstream release tarballs (spectool -g semaphore.spec) +semaphore_community_*_linux_amd64.tar.gz +semaphore_community_*_linux_arm64.tar.gz diff --git a/README.md b/README.md new file mode 100644 index 0000000..74c2082 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# semaphore + +Semaphore UI (https://semaphoreui.com) packaged for InfoMaOS. Web UI and +API for running Ansible, Terraform, OpenTofu, Terragrunt, Bash and +PowerShell tasks. + +Binaries are the upstream **community** (fully open source, MIT) release +builds. Download the source tarballs with: + + spectool -g semaphore.spec + +## Subpackages + +- `semaphore` — common base: `semaphore` user/group (uid/gid 127), + `/etc/semaphore/config.json`, `/etc/semaphore/env` (secrets generated + on first install), `/var/lib/semaphore` data directory. +- `semaphore-service` — native binary + `semaphore.service` unit. + Requires `git` and `ansible`. Conflicts with `semaphore-container`. +- `semaphore-container` — Podman quadlet running + `docker.io/semaphoreui/semaphore:v` in a pod + (`semaphore-pod`, web UI published on port 3000). The image bundles + Ansible, Terraform, OpenTofu and Terragrunt. Conflicts with + `semaphore-service`. +- `semaphore-postgresql` — optional PostgreSQL backend as a + `semaphore-db` quadlet container. With `-service` it runs standalone, + published on `127.0.0.1:5432`; with `-container` it joins the + semaphore pod. Without it, Semaphore uses SQLite at + `/var/lib/semaphore/database.sqlite` (zero configuration). + +## Database + +Default dialect is SQLite (`config.json`). Installing +`semaphore-postgresql` switches the dialect to PostgreSQL via +`SEMAPHORE_DB_*` environment drop-ins: + +- service mode: `/etc/systemd/system/semaphore.service.d/database.conf` +- container mode: `/usr/share/containers/systemd/semaphore.container.d/database.conf` + +Change the default `semaphore`/`semaphore` credentials in **both** the +relevant `database.conf` and +`/etc/containers/systemd/semaphore-db.container.d/credentials.conf` +before first start. + +## First start + +```bash +systemctl enable --now semaphore.service # service mode +systemctl start semaphore.service # container mode (enabled via pod drop-in) + +# Create the initial admin user +semaphore user add --admin --login admin --name Admin \ + --email admin@example.org --password '' \ + --config /etc/semaphore/config.json +``` + +In container mode `/usr/bin/semaphore` is a shim that runs the CLI +inside the running container. + +Session/encryption secrets (`SEMAPHORE_COOKIE_HASH`, +`SEMAPHORE_COOKIE_ENCRYPTION`, `SEMAPHORE_ACCESS_KEY_ENCRYPTION`) are +generated into `/etc/semaphore/env` by the base package `%post` on first +install. Back up this file — `SEMAPHORE_ACCESS_KEY_ENCRYPTION` encrypts +the stored SSH keys and secrets, which are lost if it changes. + +## Updating + +1. Bump `Version` in the spec and the image tag in + `semaphore.container`. +2. `spectool -g semaphore.spec` +3. `./build.sh` diff --git a/config.json b/config.json new file mode 100644 index 0000000..547eb48 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "dialect": "sqlite", + "sqlite": { + "host": "/var/lib/semaphore/database.sqlite" + }, + "port": ":3000", + "tmp_path": "/var/lib/semaphore/tmp" +} diff --git a/semaphore-container-database.conf b/semaphore-container-database.conf new file mode 100644 index 0000000..ba6011c --- /dev/null +++ b/semaphore-container-database.conf @@ -0,0 +1,14 @@ +# Switches the semaphore container from the default SQLite database to +# the semaphore-db PostgreSQL container running in the same pod. +# Credentials must match +# /etc/containers/systemd/semaphore-db.container.d/credentials.conf. +[Unit] +Wants=semaphore-db.service +After=semaphore-db.service + +[Container] +Environment=SEMAPHORE_DB_DIALECT=postgres +Environment=SEMAPHORE_DB_HOST=127.0.0.1:5432 +Environment=SEMAPHORE_DB_USER=semaphore +Environment=SEMAPHORE_DB_PASS=semaphore +Environment=SEMAPHORE_DB=semaphore diff --git a/semaphore-database.conf b/semaphore-database.conf new file mode 100644 index 0000000..eb64873 --- /dev/null +++ b/semaphore-database.conf @@ -0,0 +1,13 @@ +# Switches semaphore.service (native binary) from the default SQLite +# database to the semaphore-db PostgreSQL container. Credentials must +# match /etc/containers/systemd/semaphore-db.container.d/credentials.conf. +[Unit] +Wants=semaphore-db.service +After=semaphore-db.service + +[Service] +Environment=SEMAPHORE_DB_DIALECT=postgres +Environment=SEMAPHORE_DB_HOST=127.0.0.1:5432 +Environment=SEMAPHORE_DB_USER=semaphore +Environment=SEMAPHORE_DB_PASS=semaphore +Environment=SEMAPHORE_DB=semaphore diff --git a/semaphore-db-credentials.conf b/semaphore-db-credentials.conf new file mode 100644 index 0000000..f265a09 --- /dev/null +++ b/semaphore-db-credentials.conf @@ -0,0 +1,4 @@ +[Container] +Environment=POSTGRES_USER=semaphore +Environment=POSTGRES_PASSWORD=semaphore +Environment=POSTGRES_DB=semaphore diff --git a/semaphore-db-pod.conf b/semaphore-db-pod.conf new file mode 100644 index 0000000..721e4eb --- /dev/null +++ b/semaphore-db-pod.conf @@ -0,0 +1,2 @@ +[Container] +Pod=semaphore.pod diff --git a/semaphore-db-publish.conf b/semaphore-db-publish.conf new file mode 100644 index 0000000..a4a366e --- /dev/null +++ b/semaphore-db-publish.conf @@ -0,0 +1,6 @@ +# Used with semaphore-service: the database container runs standalone +# (outside the pod), so it must publish its port on the loopback and run +# directly as the host semaphore user to own the data directory. +[Container] +User=127:127 +PublishPort=127.0.0.1:5432:5432 diff --git a/semaphore-db.container b/semaphore-db.container new file mode 100644 index 0000000..d12d3b6 --- /dev/null +++ b/semaphore-db.container @@ -0,0 +1,21 @@ +[Unit] +Description=Semaphore UI PostgreSQL Database + +[Container] +Image=docker.io/library/postgres:18.1-trixie +ContainerName=semaphore-db +User=1001:1001 +Volume=/var/lib/semaphore/postgresql:/var/lib/postgresql:z +Exec=postgres -c shared_buffers=256MB -c min_wal_size=80MB +HealthCmd=pg_isready -U semaphore +HealthInterval=30s +HealthTimeout=10s +HealthRetries=5 +HealthStartPeriod=1m +HealthOnFailure=kill +LogDriver=journald + +[Service] +Restart=always +RestartSec=5 +TimeoutStartSec=900 diff --git a/semaphore-pod-enable.conf b/semaphore-pod-enable.conf new file mode 100644 index 0000000..f612d8f --- /dev/null +++ b/semaphore-pod-enable.conf @@ -0,0 +1,2 @@ +[Install] +WantedBy=multi-user.target diff --git a/semaphore-shim b/semaphore-shim new file mode 100644 index 0000000..b505a68 --- /dev/null +++ b/semaphore-shim @@ -0,0 +1,2 @@ +#!/bin/sh +exec podman exec -i semaphore semaphore "$@" diff --git a/semaphore.container b/semaphore.container new file mode 100644 index 0000000..26b58c9 --- /dev/null +++ b/semaphore.container @@ -0,0 +1,25 @@ +[Unit] +Description=Semaphore UI - modern UI for Ansible, Terraform and OpenTofu + +[Container] +Image=docker.io/semaphoreui/semaphore:v2.19.8 +ContainerName=semaphore +Pod=semaphore.pod +Volume=/var/lib/semaphore:/var/lib/semaphore:z +Volume=/etc/semaphore:/etc/semaphore:z +Volume=/etc/localtime:/etc/localtime:ro +EnvironmentFile=/etc/semaphore/env +Environment=SEMAPHORE_CONFIG_PATH=/etc/semaphore +Notify=healthy +HealthCmd=curl --fail --silent --output /dev/null http://localhost:3000/api/ping +HealthInterval=30s +HealthTimeout=10s +HealthRetries=5 +HealthStartPeriod=120s +HealthOnFailure=kill +LogDriver=journald + +[Service] +Restart=always +RestartSec=5 +TimeoutStartSec=900 diff --git a/semaphore.env b/semaphore.env new file mode 100644 index 0000000..37f38bd --- /dev/null +++ b/semaphore.env @@ -0,0 +1,6 @@ +# Environment for Semaphore UI (read by semaphore.service and the +# semaphore quadlet container). Secrets are generated on first install. +SEMAPHORE_CONFIG=/etc/semaphore/config.json +SEMAPHORE_COOKIE_HASH= +SEMAPHORE_COOKIE_ENCRYPTION= +SEMAPHORE_ACCESS_KEY_ENCRYPTION= diff --git a/semaphore.pod b/semaphore.pod new file mode 100644 index 0000000..da6b483 --- /dev/null +++ b/semaphore.pod @@ -0,0 +1,5 @@ +[Pod] +PodName=semaphore-pod +PublishPort=3000:3000 +UIDMap=0:0:127 127:1001:1 128:128:873 1001:127:1 1002:1002:64535 +GIDMap=0:0:127 127:1001:1 128:128:873 1001:127:1 1002:1002:64535 diff --git a/semaphore.service b/semaphore.service new file mode 100644 index 0000000..e613dd7 --- /dev/null +++ b/semaphore.service @@ -0,0 +1,18 @@ +[Unit] +Description=Semaphore UI - modern UI for Ansible, Terraform and OpenTofu +Documentation=https://docs.semaphoreui.com/ +Wants=network-online.target +After=network-online.target + +[Service] +User=semaphore +Group=semaphore +WorkingDirectory=/var/lib/semaphore +EnvironmentFile=/etc/semaphore/env +ExecStart=/usr/libexec/semaphore server --config /etc/semaphore/config.json +ExecReload=/bin/kill -HUP $MAINPID +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/semaphore.spec b/semaphore.spec new file mode 100644 index 0000000..23d71ea --- /dev/null +++ b/semaphore.spec @@ -0,0 +1,207 @@ +Name: semaphore +Version: 2.19.8 +Release: 1%{?dist} +Summary: Semaphore UI - modern UI for Ansible, Terraform, OpenTofu and other DevOps tools +License: MIT +Group: System Environment/Base +URL: https://semaphoreui.com + +# Upstream community (fully open source) release binaries, download with +# spectool -g semaphore.spec +Source0: https://github.com/semaphoreui/semaphore/releases/download/v%{version}/semaphore_community_%{version}_linux_amd64.tar.gz +Source1: https://github.com/semaphoreui/semaphore/releases/download/v%{version}/semaphore_community_%{version}_linux_arm64.tar.gz +Source10: semaphore.service +Source11: semaphore.sysusers +Source12: config.json +Source13: semaphore.env +Source14: semaphore-shim +Source15: semaphore.container +Source16: semaphore.pod +Source17: semaphore-pod-enable.conf +Source18: semaphore-database.conf +Source19: semaphore-container-database.conf +Source20: semaphore-db.container +Source21: semaphore-db-pod.conf +Source22: semaphore-db-publish.conf +Source23: semaphore-db-credentials.conf + +ExclusiveArch: x86_64 aarch64 + +BuildRequires: systemd-rpm-macros + +%description +Semaphore UI is a modern web interface and API for running Ansible +playbooks, Terraform, OpenTofu, Terragrunt, Bash and PowerShell tasks +with notifications, access control and audit trail. + +%package service +Summary: Semaphore UI systemd service (native binary) +Requires: %{name} = %{version}-%{release} +Requires: git +Requires: ansible +Conflicts: %{name}-container +%{?systemd_requires} + +%description service +Runs Semaphore UI as a native binary under systemd. Includes the +semaphore binary and systemd unit. Uses the SQLite database by default. + +%package container +Summary: Semaphore UI container (Podman quadlet) +Requires: %{name} = %{version}-%{release} +Requires: podman +Requires: containers-common +Conflicts: %{name}-service + +%description container +Runs Semaphore UI as a Podman container via quadlet. The upstream image +bundles Ansible, Terraform, OpenTofu and Terragrunt. + +%package postgresql +Summary: PostgreSQL database support for Semaphore UI +Requires: podman +Requires: containers-common +Requires: %{name} = %{version}-%{release} + +%description postgresql +Provides PostgreSQL database integration for Semaphore UI. When used +with semaphore-service, runs a standalone PostgreSQL container +(semaphore-db) published on the loopback. When used with +semaphore-container, runs the PostgreSQL container in the semaphore pod. + +%install +%{__rm} -rf %{buildroot} + +# Binary (for -service subpackage) → libexec +install -d -m 755 %{buildroot}%{_libexecdir} +%ifarch x86_64 +tar -xzf %{SOURCE0} -C . semaphore +%endif + +%ifarch aarch64 +tar -xzf %{SOURCE1} -C . semaphore +%endif +install -m 755 semaphore %{buildroot}%{_libexecdir}/semaphore + +# Shim (for -container subpackage) +install -m 755 %{SOURCE14} %{buildroot}%{_libexecdir}/semaphore-container + +# Base: sysusers, config, env +install -p -D -m 644 %{SOURCE11} %{buildroot}%{_sysusersdir}/semaphore.conf + +install -d -m 750 %{buildroot}%{_sysconfdir}/semaphore +install -m 640 %{SOURCE12} %{buildroot}%{_sysconfdir}/semaphore/config.json +install -m 600 %{SOURCE13} %{buildroot}%{_sysconfdir}/semaphore/env + +# Base: data dirs +install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore +install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore/tmp + +# Service subpackage +install -p -D -m 644 %{SOURCE10} %{buildroot}%{_unitdir}/semaphore.service +install -p -D -m 644 %{SOURCE22} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container.d/publish.conf + +# Container subpackage +install -p -D -m 644 %{SOURCE15} %{buildroot}%{_datadir}/containers/systemd/semaphore.container +install -p -D -m 644 %{SOURCE16} %{buildroot}%{_datadir}/containers/systemd/semaphore.pod +install -p -D -m 644 %{SOURCE17} %{buildroot}%{_sysconfdir}/containers/systemd/semaphore.pod.d/enable.conf + +# PostgreSQL subpackage +install -d -m 750 %{buildroot}%{_sharedstatedir}/semaphore/postgresql +install -p -D -m 644 %{SOURCE18} %{buildroot}%{_sysconfdir}/systemd/system/semaphore.service.d/database.conf +install -p -D -m 644 %{SOURCE19} %{buildroot}%{_datadir}/containers/systemd/semaphore.container.d/database.conf +install -p -D -m 644 %{SOURCE20} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container +install -p -D -m 644 %{SOURCE21} %{buildroot}%{_datadir}/containers/systemd/semaphore-db.container.d/pod.conf +install -p -D -m 644 %{SOURCE23} %{buildroot}%{_sysconfdir}/containers/systemd/semaphore-db.container.d/credentials.conf + +%pre +%sysusers_create_compat %{SOURCE11} + +%post +# Generate session/encryption secrets on first install (empty values only) +for key in SEMAPHORE_COOKIE_HASH SEMAPHORE_COOKIE_ENCRYPTION SEMAPHORE_ACCESS_KEY_ENCRYPTION; do + if grep -q "^${key}=$" %{_sysconfdir}/semaphore/env 2>/dev/null; then + secret=$(head -c 32 /dev/urandom | base64 -w0) + sed -i "s|^${key}=$|${key}=${secret}|" %{_sysconfdir}/semaphore/env + fi +done + +%post service +%systemd_post semaphore.service + +%posttrans service +ln -sf %{_libexecdir}/semaphore %{_bindir}/semaphore + +%preun service +%systemd_preun semaphore.service + +%postun service +%systemd_postun semaphore.service +if [ $1 -eq 0 ]; then + rm -f %{_bindir}/semaphore +fi + +%post container +%systemd_post semaphore.service + +%posttrans container +ln -sf %{_libexecdir}/semaphore-container %{_bindir}/semaphore + +%preun container +%systemd_preun semaphore.service +if [ $1 -eq 0 ]; then + rm -f %{_bindir}/semaphore +fi + +%postun container +%systemd_postun semaphore.service + +%clean +%{__rm} -rf %{buildroot} + +%files +%defattr(-,root,root,-) +%{_sysusersdir}/semaphore.conf + +%dir %attr(0750,semaphore,semaphore) %{_sysconfdir}/semaphore +%config(noreplace) %attr(0640,semaphore,semaphore) %{_sysconfdir}/semaphore/config.json +%config(noreplace) %attr(0600,root,root) %{_sysconfdir}/semaphore/env + +%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore +%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore/tmp + +%files service +%defattr(-,root,root,-) +%{_libexecdir}/semaphore +%ghost %{_bindir}/semaphore +%attr(0644,root,root) %{_unitdir}/semaphore.service +%dir %{_datadir}/containers/systemd/semaphore-db.container.d +%{_datadir}/containers/systemd/semaphore-db.container.d/publish.conf + +%files container +%defattr(-,root,root,-) +%attr(0755,root,root) %{_libexecdir}/semaphore-container +%ghost %{_bindir}/semaphore +%{_datadir}/containers/systemd/semaphore.container +%{_datadir}/containers/systemd/semaphore.pod +%dir %{_sysconfdir}/containers/systemd/semaphore.pod.d +%config(noreplace) %{_sysconfdir}/containers/systemd/semaphore.pod.d/enable.conf + +%files postgresql +%defattr(-,root,root,-) +%dir %attr(0750,semaphore,semaphore) %{_sharedstatedir}/semaphore/postgresql +# For -service: systemd drop-in switching the native service to PostgreSQL +%dir %{_sysconfdir}/systemd/system/semaphore.service.d +%config(noreplace) %{_sysconfdir}/systemd/system/semaphore.service.d/database.conf +# For -container: semaphore-db container + drop-in wiring it to semaphore +%{_datadir}/containers/systemd/semaphore-db.container +%dir %{_datadir}/containers/systemd/semaphore.container.d +%config(noreplace) %{_datadir}/containers/systemd/semaphore.container.d/database.conf +%dir %{_datadir}/containers/systemd/semaphore-db.container.d +%{_datadir}/containers/systemd/semaphore-db.container.d/pod.conf +%dir %{_sysconfdir}/containers/systemd/semaphore-db.container.d +%config(noreplace) %{_sysconfdir}/containers/systemd/semaphore-db.container.d/credentials.conf + +%changelog +* Tue Aug 18 2026 Zoran Pericic - 2.19.8-1 +- Initial package with service, container and postgresql subpackages diff --git a/semaphore.sysusers b/semaphore.sysusers new file mode 100644 index 0000000..d74123f --- /dev/null +++ b/semaphore.sysusers @@ -0,0 +1,2 @@ +g semaphore 127 +u semaphore 127:127 "Semaphore UI" /var/lib/semaphore /sbin/nologin