name: release # Construit l'image du plan de contrôle et publie le bundle d'agent. # # Prérequis côté Gitea : # - un secret REGISTERYKEY (jeton d'accès avec les portées repository + package) # - les paquets activés sur l'instance # - un runner ubuntu-latest ayant accès au socket Docker # - un runner windows-latest (test de fumée de l'agent) on: push: branches: [main] tags: ['v*'] workflow_dispatch: env: IMAGE_NAME: stream-control-server PACKAGE_NAME: stream-control-agent jobs: build: runs-on: ubuntu-latest outputs: version: ${{ steps.meta.outputs.version }} registry: ${{ steps.meta.outputs.registry }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '24' # --- Identité de la version ----------------------------------------- - name: Calculer version et registre id: meta run: | set -euo pipefail # Une étiquette v1.2.3 donne 1.2.3 ; sinon 0.1.0-. if [[ "${GITHUB_REF}" == refs/tags/* ]]; then VERSION="${GITHUB_REF#refs/tags/}" VERSION="${VERSION#v}" else VERSION="0.1.0-${GITHUB_SHA:0:7}" fi # L'hôte du registre est celui de l'instance Gitea. La variable # REGISTRY_HOST permet de le forcer (instance derrière un autre nom). REGISTRY="${{ vars.REGISTRY_HOST }}" if [ -z "$REGISTRY" ]; then REGISTRY="${GITHUB_SERVER_URL#http://}" REGISTRY="${REGISTRY#https://}" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT" echo "Version $VERSION → registre $REGISTRY" # --- Qualité --------------------------------------------------------- - name: Installer les dépendances run: npm ci - name: Contrôle de types run: npm run typecheck - name: Compiler run: npm run build - name: Produire le bundle d'agent run: npm run bundle -w @stream-control/agent # Un bundle qui ne démarre pas ne doit jamais être publié. Le serveur est # volontairement injoignable ici : on vérifie que le diagnostic s'exécute. - name: Vérifier le bundle (Linux) env: AGENT_TOKEN: ci-smoke-test SERVER_URL: ws://127.0.0.1:1/ws/agent run: | set -euo pipefail OUT="$(node packages/agent/dist-bundle/agent.cjs --check 2>&1 || true)" echo "$OUT" echo "$OUT" | grep -q 'obs-websocket' || { echo 'Diagnostic incomplet'; exit 1; } # --- Image du plan de contrôle --------------------------------------- - name: Authentification au registre run: | echo "${{ secrets.REGISTERYKEY }}" \ | docker login "${{ steps.meta.outputs.registry }}" \ --username "${{ github.repository_owner }}" --password-stdin - name: Construire et pousser l'image run: | set -euo pipefail BASE="${{ steps.meta.outputs.registry }}/${{ github.repository_owner }}/${IMAGE_NAME}" VERSION="${{ steps.meta.outputs.version }}" docker build \ --tag "$BASE:$VERSION" \ --tag "$BASE:sha-${GITHUB_SHA:0:7}" \ --label "org.opencontainers.image.revision=$GITHUB_SHA" \ --label "org.opencontainers.image.version=$VERSION" \ --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/${{ github.repository }}" \ . docker push "$BASE:$VERSION" docker push "$BASE:sha-${GITHUB_SHA:0:7}" # :latest ne suit que main et les étiquettes, jamais une branche de travail. if [[ "$GITHUB_REF" == refs/heads/main || "$GITHUB_REF" == refs/tags/* ]]; then docker tag "$BASE:$VERSION" "$BASE:latest" docker push "$BASE:latest" fi # --- Bundle d'agent et scripts d'installation ------------------------ - name: Publier les artefacts d'agent run: | set -euo pipefail API="${GITHUB_SERVER_URL}/api/packages/${{ github.repository_owner }}/generic/${PACKAGE_NAME}" AUTH="${{ github.repository_owner }}:${{ secrets.REGISTERYKEY }}" VERSION="${{ steps.meta.outputs.version }}" publish() { local version="$1" file="$2" path="$3" curl -sS -f -X PUT --user "$AUTH" --upload-file "$path" \ "$API/$version/$file" echo " publié : $version/$file" } # Version immuable, tracée. publish "$VERSION" agent.cjs packages/agent/dist-bundle/agent.cjs publish "$VERSION" install-agent.sh deploy/install-agent.sh publish "$VERSION" install-agent.ps1 deploy/install-agent.ps1 # Alias « latest », que visent les commandes d'installation. Le registre # générique refuse d'écraser un fichier : on supprime la version d'abord. if [[ "$GITHUB_REF" == refs/heads/main || "$GITHUB_REF" == refs/tags/* ]]; then curl -sS -X DELETE --user "$AUTH" "$API/latest" >/dev/null || true publish latest agent.cjs packages/agent/dist-bundle/agent.cjs publish latest install-agent.sh deploy/install-agent.sh publish latest install-agent.ps1 deploy/install-agent.ps1 fi - name: Récapitulatif run: | BASE="${{ steps.meta.outputs.registry }}/${{ github.repository_owner }}/${IMAGE_NAME}" { echo "### Publié" echo "" echo "| Artefact | Référence |" echo "| --- | --- |" echo "| Image | \`$BASE:${{ steps.meta.outputs.version }}\` |" echo "| Agent | \`${PACKAGE_NAME}/${{ steps.meta.outputs.version }}/agent.cjs\` |" } >> "$GITHUB_STEP_SUMMARY" # Le bundle doit démarrer sur Windows : chemins, PowerShell, résolution de # modules. Un test sur Linux seul ne couvre pas ces différences. verify-windows: runs-on: windows-latest needs: build steps: - uses: actions/checkout@v4 # Le runner fournit Node 20, alors que le projet exige 22+ et que les # scripts d'installation déposent 22+ sur les VM. Sans cette étape, le # test de fumée validerait le bundle contre une version que la production # n'utilisera jamais. - uses: actions/setup-node@v4 with: node-version: '22' # Deux pièges de Windows PowerShell 5.1 sont à l'œuvre dans les étapes # `shell: powershell` ci-dessous, et valent d'être connus avant de les # modifier : # # 1. Les guillemets doubles imbriqués dans un argument passé à un # exécutable natif sont perdus. `node -p 'x.split(".")[0]'` arrive # donc comme `x.split(.)[0]` et lève une SyntaxError. On lit ici # `node --version` et on découpe côté PowerShell : rien ne traverse # cette frontière. # 2. Le runner écrit ce bloc dans un .ps1 sans BOM, que PowerShell 5.1 # relit dans la page de codes ANSI. Tout caractère non-ASCII y devient # illisible (« trouvé » s'affichait « trouvǸ »). Les chaînes affichées # restent donc en ASCII — les commentaires, eux, ne sont jamais rendus. - name: Confirmer la version de Node shell: powershell run: | $raw = (& node --version) if ($raw -match '^v(\d+)\.') { $major = [int]$Matches[1] } else { $major = 0 } Write-Host "Node $raw" if ($major -lt 22) { throw "Node 22+ requis, version lue : $raw" } # Le script d'installation Windows n'est exécutable que sur Windows : # ce contrôle de syntaxe est la seule barrière avant qu'il n'atterrisse # sur une VM de production. # shell: powershell (Windows PowerShell 5.1) et non pwsh : PowerShell 7 # n'est pas garanti sur un runner Windows, alors que 5.1 est toujours là. # C'est aussi la version que les VM d'enregistrement exécuteront. - name: Valider la syntaxe du script d'installation shell: powershell run: | $errors = $null [System.Management.Automation.Language.Parser]::ParseFile( (Resolve-Path 'deploy/install-agent.ps1'), [ref]$null, [ref]$errors) | Out-Null if ($errors) { $errors | ForEach-Object { Write-Host "$($_.Extent.StartLineNumber): $($_.Message)" } throw "$($errors.Count) erreur(s) de syntaxe dans install-agent.ps1" } Write-Host 'install-agent.ps1 : syntaxe valide.' - name: Récupérer le bundle publié shell: powershell run: | $ErrorActionPreference = 'Stop' # PowerShell 5.1 ne négocie pas TLS 1.2 par défaut sur toutes les éditions. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $url = "$env:GITHUB_SERVER_URL/api/packages/${{ github.repository_owner }}/generic/$env:PACKAGE_NAME/${{ needs.build.outputs.version }}/agent.cjs" $pair = "${{ github.repository_owner }}:${{ secrets.REGISTERYKEY }}" $auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair)) Invoke-WebRequest -Uri $url -Headers @{ Authorization = "Basic $auth" } ` -OutFile agent.cjs -UseBasicParsing Write-Host "Bundle : $((Get-Item agent.cjs).Length) octets" - name: Exécuter le diagnostic shell: powershell env: AGENT_TOKEN: ci-smoke-test SERVER_URL: ws://127.0.0.1:1/ws/agent run: | $out = (& node agent.cjs --check 2>&1 | Out-String) $code = $LASTEXITCODE Write-Host $out Write-Host "code de sortie du diagnostic : $code" # On valide que le diagnostic s'est déroulé, pas son verdict. foreach ($marker in @('obs-websocket', 'powershell')) { if ($out -notmatch [regex]::Escape($marker)) { throw "Diagnostic incomplet : marqueur $marker absent de la sortie" } } Write-Host 'Bundle fonctionnel sous Windows.' # --check sort en 1 : le serveur est injoignable par construction ici. # Sans ce exit explicite, le wrapper PowerShell propage $LASTEXITCODE # et fait échouer l'étape malgré des assertions vertes. exit 0