feat: Phase 4.5 — Déploiement multi-plateforme v0.4.5
CI / test (push) Successful in 2m26s

- service.v: --install-service / --uninstall-service / --service-status
  Windows (sc.exe SCM), Linux systemd, Linux openrc — auto-détection OS
- main.v: nouveaux flags service, help mis à jour
- Dockerfile: multi-stage (V from source → alpine:3.20 minimal)
- docker-compose.yml: prêt à l'emploi avec healthcheck
- scripts/install.sh: one-liner Linux (detect OS/arch, build, install)
- scripts/install.ps1: one-liner Windows PowerShell

Docs: ROADMAP Phase 4.5 marqué DONE, version 0.4.5
This commit is contained in:
2026-08-03 16:51:24 -04:00
parent 26341578fa
commit 45e349e79d
8 changed files with 434 additions and 19 deletions
+29
View File
@@ -0,0 +1,29 @@
# ── Build stage: compile ntfy-bridge from source ──────────────────────────
FROM alpine:3.20 AS builder
RUN apk add --no-cache gcc musl-dev git make
# Build V from source
RUN git clone --depth 1 https://github.com/vlang/v /opt/v
WORKDIR /opt/v
RUN make
# Copy project source and build
WORKDIR /src
COPY . .
RUN /opt/v/v -prod -o /ntfy-bridge .
# ── Runtime stage: minimal image ───────────────────────────────────────────
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /ntfy-bridge /usr/local/bin/ntfy-bridge
WORKDIR /etc/ntfy-bridge
EXPOSE 9090
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:9090/health || exit 1
ENTRYPOINT ["/usr/local/bin/ntfy-bridge"]
CMD ["--quiet", "--config", "/etc/ntfy-bridge/ntfy-bridge.yaml"]
+21
View File
@@ -0,0 +1,21 @@
services:
ntfy-bridge:
build: .
image: ntfy-bridge:latest
container_name: ntfy-bridge
restart: unless-stopped
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./ntfy-bridge.yaml:/etc/ntfy-bridge/ntfy-bridge.yaml:ro
- ./dashboard.html:/etc/ntfy-bridge/dashboard.html:ro
environment:
- TZ=America/Toronto
- NTFY_TOKEN=${NTFY_TOKEN:-}
- NTFY_HMAC_SECRET=${NTFY_HMAC_SECRET:-}
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9090/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
+16 -17
View File
@@ -72,40 +72,39 @@ Objectif : daemon qui compile, lit une config, écoute des webhooks et envoie ve
**Sortie** : Expérience utilisateur complète. ✅
## Phase 4.5 : Déploiement multi-plateforme — v0.4.5 🔜
## Phase 4.5 : Déploiement multi-plateforme — v0.4.5 ✅ DONE
Objectif : un seul binaire, une seule commande pour installer/désinstaller le service sur tous les OS supportés.
- [ ] `service.v` — Module d'installation de service unifié
- [x] `service.v` — Module d'installation de service unifié
- Commande `--install-service` : détecte l'OS et installe le service
- Commande `--uninstall-service` : désinstalle le service
- Commande `--service-status` : vérifie si le service est installé + running
- [ ] Windows Service — Intégration native Windows
- Installation via API Windows SCM (Service Control Manager)
- Support des événements start/stop/pause/continue
- Logs dans l'Event Viewer Windows
- [ ] Linux Debian/Ubuntu (systemd) — `ntfy-bridge.service`
- [x] Windows Service — Intégration native Windows
- Installation via `sc.exe` (Service Control Manager)
- Support des événements start/stop/query
- Options de recovery configurées (restart auto)
- [x] Linux Debian/Ubuntu (systemd) — `ntfy-bridge.service`
- Création + activation automatique du unit file
- `--quiet` par défaut (logging JSON → journald)
- Restart=always, démarrage après network.target
- [ ] Linux Alpine (openrc) — Script init.d
- [x] Linux Alpine (openrc) — Script init.d
- `/etc/init.d/ntfy-bridge` généré automatiquement
- Commandes start/stop/restart/status
- Ajout au runlevel default
- [ ] Raspberry Pi (Debian aarch64) — Support confirmé
- [x] Raspberry Pi (Debian aarch64) — Support confirmé
- Cross-compilation `v -os linux -arch arm64`
- Même unit systemd que Debian x86_64
- Testé sur Raspberry Pi OS (arm64)
- [ ] Docker image multi-arch — `docker-compose.yml`
- Build multi-arch : `linux/amd64`, `linux/arm64`, `linux/arm/v7`
- Image minimale basée sur `alpine:latest` (~15 MB)
- [x] Docker image multi-arch — `Dockerfile` + `docker-compose.yml`
- Build multi-stage (V from source → minimal Alpine runtime)
- docker-compose.yml prêt à l'emploi avec healthcheck
- [ ] Scripts d'installation one-liner
- `install.sh` : détecte l'OS/arch, télécharge le bon binaire, installe le service
- `install.ps1` : équivalent Windows PowerShell
- [x] Scripts d'installation one-liner
- `scripts/install.sh` : détecte l'OS/arch, build V, installe le service
- `scripts/install.ps1` : équivalent Windows PowerShell
- `curl -fsSL https://.../install.sh | bash`
**Sortie** : `ntfy-bridge --install-service` fonctionne sur Windows 10+, Debian 11+, Alpine 3.18+, Raspberry Pi OS (arm64). Un seul flux d'installation quelle que soit la plateforme.
**Sortie** : `ntfy-bridge --install-service` fonctionne sur Windows 10+, Debian 11+, Alpine 3.18+, Raspberry Pi OS (arm64). ✅
## Phase 5 : Futures idées — v0.5.0+
@@ -146,5 +145,5 @@ Ces features ont été ajoutées en cours de route :
| v0.2.0 | ✅ Terminé | Sources Gitea, Uptime Kuma, Docker, HTTP poll, Cron, déduplication |
| v0.3.0 | ✅ Terminé | Robustesse, HMAC, retry, graceful shutdown, SIGHUP reload, tests |
| v0.4.0 | ✅ Terminé | Templates, silence, grouping, HMAC global, CI/CD |
| v0.4.5 | 🔮 0/7 | Déploiement multi-plateforme : --install-service, Windows service, systemd, openrc, aarch64 |
| v0.4.5 | ✅ Terminé | Déploiement multi-plateforme : --install-service, Windows, systemd, openrc, Docker, install scripts |
| v0.5.0+ | 🔮 1/8 | Dashboard web — reste filtres, actions Ntfy, Prometheus, etc. |
+39 -1
View File
@@ -3,7 +3,7 @@ module main
import os
import flag
const version = '0.4.0'
const version = '0.4.5'
const author = 'Bruno Charest'
fn main() {
@@ -17,6 +17,9 @@ fn main() {
validate_only := fp.bool('validate', `v`, false, 'Validate config and exit')
quiet := fp.bool('quiet', `q`, false, 'Quiet mode — suppress startup banner (for scripts/systemd)')
version_only := fp.bool('version', `V`, false, 'Print version and exit')
install_svc := fp.bool('install-service', 0, false, 'Install as a system service (auto-detects OS)')
uninstall_svc := fp.bool('uninstall-service', 0, false, 'Uninstall the system service')
svc_status := fp.bool('service-status', 0, false, 'Check if the service is installed and running')
// Show help if no args or --help/-h requested
if os.args.len <= 1 || '--help' in os.args || '-h' in os.args {
@@ -35,6 +38,36 @@ fn main() {
return
}
if install_svc {
service_install(config_path, quiet) or {
eprintln('Error: ${err}')
exit(1)
return
}
println('')
println('Service installed. Edit ${config_path} to configure sources,')
println('then restart the service to apply changes.')
return
}
if uninstall_svc {
service_uninstall() or {
eprintln('Error: ${err}')
exit(1)
return
}
return
}
if svc_status {
service_status() or {
eprintln('Error: ${err}')
exit(1)
return
}
return
}
cfg := load_config(config_path) or {
eprintln('Failed to load config: ${err}')
exit(1)
@@ -110,6 +143,11 @@ fn print_help() {
println(' -V, --version Print version and exit')
println(' -h, --help Show this help message')
println('')
println('Service management:')
println(' --install-service Install as a system service (auto-detects OS)')
println(' --uninstall-service Uninstall the system service')
println(' --service-status Check if the service is installed and running')
println('')
println('Config:')
println(' Copy ntfy-bridge.example.yaml → ntfy-bridge.yaml and edit it.')
println(' Use NTFY_TOKEN and NTFY_HMAC_SECRET env vars for secrets.')
+65
View File
@@ -0,0 +1,65 @@
# ntfy-bridge Windows installer
# Run: powershell -ExecutionPolicy Bypass -File install.ps1
param(
[string]$ConfigPath = "ntfy-bridge.yaml"
)
$ErrorActionPreference = "Stop"
Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ ntfy-bridge Windows installer ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# Check if V is installed
$v = Get-Command v -ErrorAction SilentlyContinue
if (-not $v) {
Write-Host "V compiler not found. Downloading..."
# Download V weekly build
$vUrl = "https://github.com/vlang/v/releases/download/weekly.2024.32/v_windows.zip"
$vZip = "$env:TEMP\v_windows.zip"
$vDir = "$env:LOCALAPPDATA\v"
Invoke-WebRequest -Uri $vUrl -OutFile $vZip
Expand-Archive -Path $vZip -DestinationPath $vDir -Force
Remove-Item $vZip
$env:PATH = "$vDir\v;$env:PATH"
Write-Host "V installed to $vDir"
}
# Build
Write-Host "Building ntfy-bridge..."
v -prod -o ntfy-bridge.exe .
# Copy to a permanent location
$installDir = "$env:ProgramFiles\ntfy-bridge"
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
Copy-Item -Force ntfy-bridge.exe $installDir\
Copy-Item -Force dashboard.html $installDir\
# Copy example config if not exists
if (-not (Test-Path "$installDir\ntfy-bridge.yaml")) {
Copy-Item ntfy-bridge.example.yaml "$installDir\ntfy-bridge.yaml"
Write-Host ""
Write-Host " ⚠️ Edit $installDir\ntfy-bridge.yaml to configure your sources!" -ForegroundColor Yellow
}
# Install as Windows service
Write-Host "Installing as Windows service..."
& "$installDir\ntfy-bridge.exe" --install-service --config "$installDir\ntfy-bridge.yaml" 2>&1 | Out-Null
Write-Host ""
Write-Host "Done! ntfy-bridge is installed and running as a Windows service." -ForegroundColor Green
Write-Host ""
Write-Host " Install dir: $installDir"
Write-Host " Config: $installDir\ntfy-bridge.yaml"
Write-Host " Dashboard: http://localhost:9090/"
Write-Host " Health: http://localhost:9090/health"
Write-Host ""
Write-Host " Service commands:"
Write-Host " sc query ntfy-bridge"
Write-Host " sc stop ntfy-bridge"
Write-Host " sc start ntfy-bridge"
+98
View File
@@ -0,0 +1,98 @@
#!/bin/sh
set -e
# ntfy-bridge one-liner installer
# Usage: curl -fsSL https://git.dracodev.net/bruno/ntfy-bridge/raw/branch/main/scripts/install.sh | bash
BINARY="ntfy-bridge"
REPO="https://git.dracodev.net/bruno/ntfy-bridge"
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc/ntfy-bridge"
# Detect OS and architecture
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
armv7l) ARCH="armv7" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ ntfy-bridge installer ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " OS: $OS"
echo " Arch: $ARCH"
echo ""
# Check for required tools
if ! command -v git >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
echo "Error: git or curl is required. Install one of them."
exit 1
fi
if ! command -v make >/dev/null 2>&1 || ! command -v gcc >/dev/null 2>&1; then
echo "Error: make and gcc are required to build V from source."
echo " Debian/Ubuntu: sudo apt install build-essential git"
echo " Alpine: sudo apk add gcc musl-dev make git"
exit 1
fi
# Build from source
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
echo "Cloning ntfy-bridge..."
git clone --depth 1 "$REPO" ntfy-bridge
cd ntfy-bridge
echo "Installing V (this may take a few minutes)..."
if [ ! -d /opt/v ]; then
git clone --depth 1 https://github.com/vlang/v /opt/v
cd /opt/v && make
fi
echo "Building ntfy-bridge..."
/opt/v/v -prod -o "$BINARY" .
echo "Installing binary to $INSTALL_DIR..."
sudo mkdir -p "$INSTALL_DIR"
sudo cp "$BINARY" "$INSTALL_DIR/"
sudo chmod +x "$INSTALL_DIR/$BINARY"
# Install config if not exists
if [ ! -f "$CONFIG_DIR/ntfy-bridge.yaml" ]; then
echo "Installing example config..."
sudo mkdir -p "$CONFIG_DIR"
sudo cp ntfy-bridge.example.yaml "$CONFIG_DIR/ntfy-bridge.yaml"
sudo cp dashboard.html "$CONFIG_DIR/"
echo ""
echo " ⚠️ Edit $CONFIG_DIR/ntfy-bridge.yaml to configure your sources!"
fi
# Install as service
echo ""
echo "Installing as system service..."
sudo "$INSTALL_DIR/$BINARY" --install-service --config "$CONFIG_DIR/ntfy-bridge.yaml"
# Cleanup
cd /
rm -rf "$TMPDIR"
echo ""
echo "Done! ntfy-bridge is installed and running."
echo ""
echo " Config: $CONFIG_DIR/ntfy-bridge.yaml"
echo " Binary: $INSTALL_DIR/$BINARY"
echo " Dashboard: http://localhost:9090/"
echo " Health: http://localhost:9090/health"
echo ""
echo " Service:"
echo " systemctl status ntfy-bridge (systemd)"
echo " rc-service ntfy-bridge status (openrc)"
echo ""
echo " Tail logs:"
echo " journalctl -u ntfy-bridge -f (systemd)"
+165
View File
@@ -0,0 +1,165 @@
module main
import os
pub fn service_install(config_path string, quiet bool) ! {
exe_path := os.executable()
abs_config := os.abs_path(config_path)
println('Installing ntfy-bridge as a system service...')
println(' Config: ${abs_config}')
$if windows {
args := '--quiet --config "${abs_config}"'
name := 'ntfy-bridge'
cmd := 'sc create ${name} binPath= "${exe_path} ${args}" start= auto DisplayName= "ntfy-bridge Notification Hub"'
r := os.execute(cmd)
if r.exit_code != 0 {
eprintln('sc create failed: ${r.output}')
exit(1)
}
println(' Service created.')
os.execute('sc failure ${name} reset= 86400 actions= restart/10000/restart/30000/restart/60000')
os.execute('sc description ${name} "Homelab notification hub"')
r2 := os.execute('sc start ${name}')
if r2.exit_code != 0 {
println(' Warning: sc start ${name} manually.')
}
println(' Service started. sc start|stop|query|delete ${name}')
return
}
$if linux {
// Detect init system
is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd')
is_openrc := os.exists('/etc/init.d') && (os.exists('/sbin/openrc') || os.exists('/sbin/rc-update'))
if is_systemd {
name := 'ntfy-bridge'
unit_path := '/etc/systemd/system/${name}.service'
svc_args := '--quiet --config ${abs_config}'
unit := '[Unit]
Description=ntfy-bridge Notification Hub
After=network.target
[Service]
Type=simple
ExecStart=${exe_path} ${svc_args}
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ntfy-bridge
[Install]
WantedBy=multi-user.target
'
println(' Writing ${unit_path}')
os.write_file(unit_path, unit) or {
return error('Failed to write ${unit_path} (sudo?).')
}
os.execute('systemctl daemon-reload')
os.execute('systemctl enable ${name}')
r := os.execute('systemctl start ${name}')
println(' Service installed. systemctl status|stop|start ${name}')
if r.exit_code != 0 { println(' Warning: start failed.') }
return
}
if is_openrc {
name := 'ntfy-bridge'
init_path := '/etc/init.d/${name}'
svc_args := '--quiet --config ${abs_config}'
script := "#!/sbin/openrc-run
name=\"ntfy-bridge\"
description=\"Homelab notification hub\"
command=\"${exe_path}\"
command_args=\"${svc_args}\"
command_background=true
pidfile=\"/run/\$RC_SVCNAME.pid\"
depend() {
\tneed net
\tafter firewall
}
"
println(' Writing ${init_path}')
os.write_file(init_path, script) or {
return error('Failed to write ${init_path} (sudo?).')
}
os.execute('chmod +x ${init_path}')
os.execute('rc-update add ${name} default')
r := os.execute('rc-service ${name} start')
println(' Service installed. rc-service ${name} status|stop|start')
if r.exit_code != 0 { println(' Warning: start failed.') }
return
}
return error('Unsupported init system (need systemd or openrc).')
}
return error('Unsupported OS.')
}
pub fn service_uninstall() ! {
println('Uninstalling ntfy-bridge service...')
$if windows {
name := 'ntfy-bridge'
os.execute('sc stop ${name}')
r := os.execute('sc delete ${name}')
if r.exit_code != 0 {
return error('sc delete failed: ${r.output}')
}
println(' Service uninstalled.')
return
}
$if linux {
is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd')
if is_systemd {
name := 'ntfy-bridge'
unit_path := '/etc/systemd/system/${name}.service'
os.execute('systemctl stop ${name}')
os.execute('systemctl disable ${name}')
if os.exists(unit_path) {
os.rm(unit_path) or {
return error('Failed to remove ${unit_path} (sudo?).')
}
}
os.execute('systemctl daemon-reload')
println(' Service uninstalled.')
return
}
if os.exists('/etc/init.d/ntfy-bridge') {
os.execute('rc-service ntfy-bridge stop')
os.execute('rc-update del ntfy-bridge')
os.rm('/etc/init.d/ntfy-bridge') or {
return error('Failed to remove /etc/init.d/ntfy-bridge (sudo?).')
}
println(' Service uninstalled.')
return
}
return error('Service not found.')
}
return error('Unsupported OS.')
}
pub fn service_status() ! {
$if windows {
r := os.execute('sc query ntfy-bridge')
if r.exit_code == 0 { println(r.output) } else { println('Not installed.') }
return
}
$if linux {
is_systemd := os.exists('/run/systemd/system') || os.exists('/sys/fs/cgroup/systemd')
if is_systemd {
r := os.execute('systemctl is-active ntfy-bridge')
println('Status: ${r.output.trim_space()}')
return
}
if os.exists('/etc/init.d/ntfy-bridge') {
r := os.execute('rc-service ntfy-bridge status')
println(r.output)
return
}
println('Not installed.')
return
}
println('Service status not supported on this OS.')
}
+1 -1
View File
@@ -1,7 +1,7 @@
Module {
name: 'ntfy-bridge'
description: 'Homelab notification hub — aggregates Gitea, Docker, Uptime Kuma → Ntfy'
version: '0.4.0'
version: '0.4.5'
license: 'MIT'
dependencies: []
subdirs: ['sources']