CI : Deployment
Some checks failed
release / build (push) Failing after 45s
release / verify-windows (push) Has been skipped

This commit is contained in:
jeanotx32
2026-08-11 17:10:08 +02:00
parent b529417940
commit 105d07b82b
16 changed files with 1662 additions and 86 deletions

172
deploy/install-agent.ps1 Normal file
View File

@@ -0,0 +1,172 @@
<#
.SYNOPSIS
Installe (ou met à jour) l'agent Stream Control sur une VM Windows.
.DESCRIPTION
L'agent est enregistré comme tâche planifiée « à l'ouverture de session », et
non comme service Windows : un service tourne en session 0 et ne verrait ni la
fenêtre du navigateur, ni OBS.
Réexécuter le script met à jour le binaire sans toucher à l'identité d'un agent
déjà enrôlé.
.EXAMPLE
& ([scriptblock]::Create((irm 'https://gitea.exemple.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) `
-Registry 'https://gitea.exemple.com' `
-Server 'ws://control.lan:8080/ws/agent' `
-Token '<JETON_ENROLEMENT>'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string] $Registry,
[string] $Server = '',
[string] $Token = '',
[string] $Name = $env:COMPUTERNAME,
[string] $Version = 'latest',
[string] $Owner = 'jeanbon',
[string] $PackageToken = '',
[string] $ObsHost = '127.0.0.1',
[int] $ObsPort = 4455,
[string] $ObsPassword = '',
[string] $InstallDir = 'C:\stream-control-agent',
[string] $RunAsUser = "$env:USERDOMAIN\$env:USERNAME",
[string] $TaskName = 'StreamControlAgent'
)
$ErrorActionPreference = 'Stop'
$Registry = $Registry.TrimEnd('/')
function Info($msg) { Write-Host "· $msg" }
function Die($msg) { Write-Host "$msg" -ForegroundColor Red; exit 1 }
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) { Die 'À exécuter dans un PowerShell administrateur.' }
$configFile = Join-Path $InstallDir 'agent.config.json'
$isUpgrade = Test-Path $configFile
if (-not $isUpgrade) {
if (-not $Server) { Die '-Server est obligatoire pour une première installation.' }
if (-not $Token) { Die '-Token est obligatoire pour une première installation.' }
}
Write-Host ''
Write-Host 'Installation de l''agent Stream Control'
Write-Host " compte : $RunAsUser"
Write-Host " destination : $InstallDir"
Write-Host " version : $Version"
Write-Host ''
# --- Node.js ----------------------------------------------------------------
$nodeMajor = 0
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
if ($nodeCmd) {
$nodeMajor = [int]((& node -p 'process.versions.node.split(".")[0]') 2>$null)
}
if ($nodeMajor -lt 22) {
Info "Node.js 22+ requis (trouvé : $nodeMajor) — installation via winget"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Die 'winget introuvable. Installe Node.js 22+ manuellement puis relance : https://nodejs.org/'
}
& winget install --id OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements
# winget ne rafraîchit pas le PATH de la session courante.
$env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
[Environment]::GetEnvironmentVariable('Path', 'User')
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Die 'Node.js installé mais absent du PATH — rouvre PowerShell et relance le script.'
}
} else {
Info "Node.js $(& node -v) présent"
}
# --- Téléchargement du bundle -----------------------------------------------
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
$url = "$Registry/api/packages/$Owner/generic/stream-control-agent/$Version/agent.cjs"
Info "Téléchargement depuis $url"
$headers = @{}
if ($PackageToken) { $headers['Authorization'] = "token $PackageToken" }
$tmp = Join-Path $InstallDir 'agent.cjs.tmp'
try {
Invoke-WebRequest -Uri $url -Headers $headers -OutFile $tmp -UseBasicParsing
} catch {
Die "Téléchargement impossible ($($_.Exception.Message)). Vérifie -Registry, -Version, et -PackageToken si le paquet est privé."
}
& node --check $tmp 2>$null
if ($LASTEXITCODE -ne 0) { Die 'Le fichier téléchargé n''est pas un script Node valide.' }
Move-Item -Force $tmp (Join-Path $InstallDir 'agent.cjs')
# --- Configuration ----------------------------------------------------------
if ($isUpgrade) {
# Un agent déjà enrôlé détient un jeton permanent : l'écraser le ferait
# réapparaître comme un second agent dans le dashboard.
Info 'Configuration existante conservée (identité de l''agent préservée)'
} else {
$config = [ordered]@{
serverUrl = $Server
token = $Token
name = $Name
obs = [ordered]@{ host = $ObsHost; port = $ObsPort; password = $ObsPassword }
}
$config | ConvertTo-Json -Depth 5 | Set-Content -Path $configFile -Encoding UTF8
Info "Configuration écrite dans $configFile"
}
# Le fichier porte un jeton : lecture réservée au compte de l'agent et aux admins.
$acl = Get-Acl $configFile
$acl.SetAccessRuleProtection($true, $false)
foreach ($identity in @($RunAsUser, 'BUILTIN\Administrators', 'NT AUTHORITY\SYSTEM')) {
try {
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
$identity, 'FullControl', 'Allow')))
} catch {
Write-Host "! Règle d'accès ignorée pour $identity"
}
}
Set-Acl -Path $configFile -AclObject $acl
# --- Tâche planifiée --------------------------------------------------------
$nodePath = (Get-Command node).Source
$entry = Join-Path $InstallDir 'agent.cjs'
$action = New-ScheduledTaskAction -Execute $nodePath -Argument "`"$entry`"" -WorkingDirectory $InstallDir
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $RunAsUser
$principal = New-ScheduledTaskPrincipal -UserId $RunAsUser -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force | Out-Null
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-ScheduledTask -TaskName $TaskName
# --- Vérification -----------------------------------------------------------
Write-Host ''
$env:AGENT_CONFIG = $configFile
& node $entry --check
Write-Host ''
Start-Sleep -Seconds 2
$state = (Get-ScheduledTask -TaskName $TaskName).State
if ($state -eq 'Running') {
Write-Host '✓ Agent installé et démarré.' -ForegroundColor Green
} else {
Write-Host "! Tâche dans l'état « $state ». Détails :" -ForegroundColor Yellow
Get-ScheduledTaskInfo -TaskName $TaskName | Format-List TaskName, LastRunTime, LastTaskResult
}
Write-Host @"
état : Get-ScheduledTask -TaskName $TaskName
test : node $entry --check
"@

227
deploy/install-agent.sh Normal file
View File

@@ -0,0 +1,227 @@
#!/usr/bin/env bash
#
# Installe (ou met à jour) l'agent Stream Control sur une VM Linux.
#
# curl -fsSL https://gitea.exemple.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
# | sudo bash -s -- \
# --registry https://gitea.exemple.com \
# --server ws://control.lan:8080/ws/agent \
# --token <JETON_ENROLEMENT>
#
# Réexécuter le script met à jour le binaire sans toucher à l'identité d'un
# agent déjà enrôlé.
set -euo pipefail
REGISTRY=""
OWNER="jeanbon"
VERSION="latest"
PACKAGE_NAME="stream-control-agent"
PACKAGE_TOKEN=""
SERVER_URL=""
TOKEN=""
AGENT_NAME="$(hostname)"
OBS_HOST="127.0.0.1"
OBS_PORT="4455"
OBS_PASSWORD=""
RUN_USER=""
INSTALL_DIR="/opt/stream-control-agent"
SERVICE="stream-control-agent"
die() { echo "$*" >&2; exit 1; }
info() { echo "· $*"; }
usage() {
cat <<'EOF'
Installe (ou met à jour) l'agent Stream Control sur une VM Linux.
curl -fsSL https://gitea.exemple.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.sh \
| sudo bash -s -- \
--registry https://gitea.exemple.com \
--server ws://control.lan:8080/ws/agent \
--token <JETON_ENROLEMENT>
Réexécuter le script met à jour le binaire sans toucher à l'identité d'un agent
déjà enrôlé.
Options :
--registry URL Base de l'instance Gitea (obligatoire)
--server URL WebSocket du plan de contrôle (obligatoire à la 1re install)
--token JETON Jeton d'enrôlement (obligatoire à la 1re install)
--name NOM Nom affiché dans le dashboard (défaut : hostname)
--user UTILISATEUR Compte exécutant l'agent (défaut : l'utilisateur sudo)
--obs-host HOTE Défaut 127.0.0.1
--obs-port PORT Défaut 4455
--obs-password MDP Mot de passe obs-websocket
--version VERSION Version du paquet à installer (défaut : latest)
--owner PROPRIETAIRE Propriétaire du paquet Gitea (défaut : jeanbon)
--package-token JETON Jeton de lecture si le paquet est privé
--help
EOF
exit 0
}
while [ $# -gt 0 ]; do
case "$1" in
--registry) REGISTRY="${2:?}"; shift 2 ;;
--server) SERVER_URL="${2:?}"; shift 2 ;;
--token) TOKEN="${2:?}"; shift 2 ;;
--name) AGENT_NAME="${2:?}"; shift 2 ;;
--user) RUN_USER="${2:?}"; shift 2 ;;
--obs-host) OBS_HOST="${2:?}"; shift 2 ;;
--obs-port) OBS_PORT="${2:?}"; shift 2 ;;
--obs-password) OBS_PASSWORD="${2:?}"; shift 2 ;;
--version) VERSION="${2:?}"; shift 2 ;;
--owner) OWNER="${2:?}"; shift 2 ;;
--package-token) PACKAGE_TOKEN="${2:?}"; shift 2 ;;
--help|-h) usage ;;
*) die "Option inconnue : $1 (--help pour l'aide)" ;;
esac
done
[ "$(id -u)" -eq 0 ] || die "À exécuter en root (sudo)."
[ -n "$REGISTRY" ] || die "--registry est obligatoire."
REGISTRY="${REGISTRY%/}"
# L'agent doit tourner sous le compte qui ouvre la session graphique d'OBS :
# c'est ce qui lui donne accès à l'affichage pour le rappel plein écran.
if [ -z "$RUN_USER" ]; then
RUN_USER="${SUDO_USER:-root}"
fi
id "$RUN_USER" >/dev/null 2>&1 || die "L'utilisateur « $RUN_USER » n'existe pas."
CONFIG_FILE="$INSTALL_DIR/agent.config.json"
IS_UPGRADE=false
[ -f "$CONFIG_FILE" ] && IS_UPGRADE=true
if [ "$IS_UPGRADE" = false ]; then
[ -n "$SERVER_URL" ] || die "--server est obligatoire pour une première installation."
[ -n "$TOKEN" ] || die "--token est obligatoire pour une première installation."
fi
echo
echo "Installation de l'agent Stream Control"
echo " utilisateur : $RUN_USER"
echo " destination : $INSTALL_DIR"
echo " version : $VERSION"
echo
# --- Node.js ----------------------------------------------------------------
NODE_MAJOR=0
if command -v node >/dev/null 2>&1; then
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
fi
if [ "$NODE_MAJOR" -lt 22 ]; then
info "Node.js 22+ requis (trouvé : ${NODE_MAJOR:-aucun}) — installation depuis NodeSource"
command -v apt-get >/dev/null 2>&1 \
|| die "Distribution non gérée automatiquement : installe Node.js 22+ puis relance."
curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
apt-get install -y nodejs
else
info "Node.js $(node -v) présent"
fi
# --- xdotool (rappel plein écran) -------------------------------------------
if ! command -v xdotool >/dev/null 2>&1; then
if command -v apt-get >/dev/null 2>&1; then
info "Installation de xdotool (rappel plein écran)"
apt-get install -y xdotool >/dev/null
else
echo "! xdotool absent : le rappel plein écran sera indisponible."
fi
fi
# --- Téléchargement du bundle -----------------------------------------------
URL="$REGISTRY/api/packages/$OWNER/generic/$PACKAGE_NAME/$VERSION/agent.cjs"
info "Téléchargement depuis $URL"
mkdir -p "$INSTALL_DIR"
CURL_AUTH=()
[ -n "$PACKAGE_TOKEN" ] && CURL_AUTH=(--header "Authorization: token $PACKAGE_TOKEN")
curl -fsSL "${CURL_AUTH[@]}" -o "$INSTALL_DIR/agent.cjs.tmp" "$URL" \
|| die "Téléchargement impossible. Vérifie --registry, --version, et --package-token si le paquet est privé."
node --check "$INSTALL_DIR/agent.cjs.tmp" 2>/dev/null \
|| die "Le fichier téléchargé n'est pas un script Node valide."
mv "$INSTALL_DIR/agent.cjs.tmp" "$INSTALL_DIR/agent.cjs"
# --- Configuration ----------------------------------------------------------
if [ "$IS_UPGRADE" = true ]; then
# Un agent déjà enrôlé possède un jeton permanent : l'écraser avec le jeton
# d'enrôlement le ferait réapparaître comme un second agent dans le dashboard.
info "Configuration existante conservée (identité de l'agent préservée)"
else
umask 077
cat > "$CONFIG_FILE" <<EOF
{
"serverUrl": "$SERVER_URL",
"token": "$TOKEN",
"name": "$AGENT_NAME",
"obs": {
"host": "$OBS_HOST",
"port": $OBS_PORT,
"password": "$OBS_PASSWORD"
}
}
EOF
info "Configuration écrite dans $CONFIG_FILE"
fi
chown -R "$RUN_USER" "$INSTALL_DIR"
chmod 600 "$CONFIG_FILE"
# --- Service systemd --------------------------------------------------------
USER_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
cat > "/etc/systemd/system/$SERVICE.service" <<EOF
[Unit]
Description=Stream Control — agent OBS
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$RUN_USER
WorkingDirectory=$INSTALL_DIR
Environment=NODE_ENV=production
Environment=AGENT_CONFIG=$CONFIG_FILE
# Accès à la session graphique, nécessaire au rappel plein écran via xdotool.
Environment=DISPLAY=:0
Environment=XAUTHORITY=$USER_HOME/.Xauthority
ExecStart=$(command -v node) $INSTALL_DIR/agent.cjs
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE" >/dev/null 2>&1 || true
systemctl restart "$SERVICE"
# --- Vérification -----------------------------------------------------------
echo
sudo -u "$RUN_USER" AGENT_CONFIG="$CONFIG_FILE" \
"$(command -v node)" "$INSTALL_DIR/agent.cjs" --check || true
sleep 2
if systemctl is-active --quiet "$SERVICE"; then
echo "✓ Agent installé et démarré."
else
echo "✗ Le service n'est pas actif. Journal :"
journalctl -u "$SERVICE" -n 20 --no-pager || true
exit 1
fi
cat <<EOF
état : systemctl status $SERVICE
journal: journalctl -u $SERVICE -f
test : sudo -u $RUN_USER node $INSTALL_DIR/agent.cjs --check
EOF

View File

@@ -1,25 +0,0 @@
# Agent Ubuntu — /etc/systemd/system/stream-control-agent.service
#
# L'agent doit tourner dans la MÊME session que OBS s'il pilote un OBS graphique.
# Sur une VM headless, OBS tourne généralement sous Xvfb : garder le même User.
#
# systemctl daemon-reload && systemctl enable --now stream-control-agent
[Unit]
Description=Stream Control — agent OBS
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=obs
WorkingDirectory=/opt/stream-control-agent
Environment=NODE_ENV=production
# Chemin du fichier d'identité (jeton + agentId) — doit être inscriptible.
Environment=AGENT_CONFIG=/opt/stream-control-agent/agent.config.json
ExecStart=/usr/bin/node /opt/stream-control-agent/dist/index.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -1,41 +0,0 @@
# Installe l'agent Stream Control comme tâche planifiée Windows.
#
# L'agent doit s'exécuter dans la session interactive de l'utilisateur qui lance
# OBS : un service Windows classique tourne en session 0 et ne verrait pas OBS.
# On utilise donc une tâche « à l'ouverture de session » avec redémarrage auto.
#
# Usage (PowerShell administrateur) :
# .\windows-agent-task.ps1 -AgentDir 'C:\stream-control-agent' -RunAsUser 'VM01\obs'
param(
[string]$AgentDir = 'C:\stream-control-agent',
[string]$RunAsUser = "$env:USERDOMAIN\$env:USERNAME",
[string]$TaskName = 'StreamControlAgent'
)
$ErrorActionPreference = 'Stop'
$node = (Get-Command node).Source
$entry = Join-Path $AgentDir 'dist\index.js'
if (-not (Test-Path $entry)) {
throw "Agent introuvable : $entry — copie d'abord packages/agent (dist + node_modules) dans $AgentDir"
}
if (-not (Test-Path (Join-Path $AgentDir 'agent.config.json'))) {
Write-Warning "agent.config.json absent de $AgentDir : l'agent refusera de démarrer sans jeton."
}
$action = New-ScheduledTaskAction -Execute $node -Argument "`"$entry`"" -WorkingDirectory $AgentDir
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $RunAsUser
$principal = New-ScheduledTaskPrincipal -UserId $RunAsUser -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName $TaskName
Write-Host "Tâche « $TaskName » installée et démarrée pour $RunAsUser."
Write-Host "Journal : Get-ScheduledTaskInfo -TaskName $TaskName"