From 73d6799eb25c55cacecdcafe776d70b0bb72c0fa Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 11 May 2026 15:10:39 +0000 Subject: [PATCH] feat(ci): CI/CD container delivery for ClickOnce artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct SSH rsync with build → push to registry → Coolify pull pattern, matching the rest of the infra (decisions-court, ai-gateway, ...). Adds cdn/ Dockerfile + nginx.conf to serve .vsto/.application/.manifest/.deploy with correct MIME types behind a StripPrefix Traefik middleware. The new workflow has three jobs: - build: Windows runner, nuget restore + msbuild + dotnet test. - publish-clickonce: Windows runner, only on v* tag — sign assemblies + mage manifests + upload publish/ as artifact. - dockerize-and-deploy: Linux runner — download artifact, build & push outlook-addin-cdn: + :latest to gitea.dev.marcus-law.co.il, then trigger Coolify redeploy of app awdlvjlozz0dvn6kjlme9rd7. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/build.yml | 215 +++++++++++++++++++++++-------------- cdn/Dockerfile | 24 +++++ cdn/nginx.conf | 44 ++++++++ 3 files changed, 202 insertions(+), 81 deletions(-) create mode 100644 cdn/Dockerfile create mode 100644 cdn/nginx.conf diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 807ac1d..c0d9c95 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build OutlookAddin +name: Build and deploy OutlookAddin on: push: @@ -7,6 +7,11 @@ on: pull_request: branches: [main] +env: + REGISTRY: gitea.dev.marcus-law.co.il + CDN_IMAGE_NAME: espocrm-extensions/outlook-addin-cdn + PROVIDER_URL: 'https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto' + jobs: build: runs-on: windows-latest @@ -30,16 +35,13 @@ jobs: name: test-results path: '**/*.trx' - publish: + publish-clickonce: needs: build - if: startsWith(github.ref, 'refs/tags/v') runs-on: windows-latest + if: startsWith(github.ref, 'refs/tags/v') env: - MAGE: 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\mage.exe' + MAGE: 'C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\mage.exe' SIGNTOOL: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe' - PROVIDER_URL: 'https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto' - REMOTE_DIR: '/var/www/outlook-addin' - REMOTE_HOST: 'platform.dev.marcus-law.co.il' steps: - name: Checkout uses: actions/checkout@v4 @@ -53,19 +55,45 @@ jobs: throw "Tag $tag is not in vMAJOR.MINOR.PATCH form" } $version = "$($Matches[1]).0" - "version=$version" | Out-File -Append $env:GITHUB_OUTPUT - "versionDot=$($Matches[1])" | Out-File -Append $env:GITHUB_OUTPUT + "version=$version" | Out-File -Append $env:GITHUB_OUTPUT + "versionDot=$($Matches[1])" | Out-File -Append $env:GITHUB_OUTPUT "versionUscore=$($version -replace '\.','_')" | Out-File -Append $env:GITHUB_OUTPUT Write-Host "Version: $version" + - name: Ensure code-signing cert in LocalMachine\My + shell: pwsh + env: + THUMB: ${{ secrets.CODESIGN_THUMBPRINT }} + PFX_B64: ${{ secrets.CODESIGN_CERT_PFX_BASE64 }} + PFX_PWD: ${{ secrets.CODESIGN_CERT_PASSWORD }} + run: | + $thumb = $env:THUMB + $found = Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue | + Where-Object { $_.Thumbprint -eq $thumb } + if ($found) { + Write-Host "Cert $thumb already in LocalMachine\My." + return + } + Write-Warning "Cert $thumb missing from LocalMachine\My. Importing from CODESIGN_CERT_PFX_BASE64." + $pfxBytes = [Convert]::FromBase64String($env:PFX_B64) + $tmpPfx = Join-Path $env:TEMP "codesign-$([Guid]::NewGuid()).pfx" + [IO.File]::WriteAllBytes($tmpPfx, $pfxBytes) + $securePwd = ConvertTo-SecureString -String $env:PFX_PWD -Force -AsPlainText + Import-PfxCertificate -FilePath $tmpPfx ` + -CertStoreLocation Cert:\LocalMachine\My ` + -Password $securePwd | Out-Null + Remove-Item -Force $tmpPfx + Write-Host "Cert imported." + - name: Restore NuGet run: nuget restore OutlookAddin.sln - name: Build Release run: msbuild OutlookAddin.sln /t:Build /p:Configuration=Release /p:Platform="Any CPU" /m - # Code signing — cert installed on the runner's CurrentUser\My store. - # Thumbprint comes from a Gitea Actions secret CODESIGN_THUMBPRINT. + # Code signing -- cert in LocalMachine\My (or CurrentUser\My); signtool + # picks it by thumbprint, the service runs as LocalSystem so the runtime + # search reaches both stores. - name: Sign assemblies shell: pwsh run: | @@ -79,45 +107,45 @@ jobs: - name: Publish ClickOnce shell: pwsh run: | - $version = "${{ steps.ver.outputs.version }}" - $vUnderscore = "${{ steps.ver.outputs.versionUscore }}" - $thumb = "${{ secrets.CODESIGN_THUMBPRINT }}" + $version = "${{ steps.ver.outputs.version }}" + $versionUscore = "${{ steps.ver.outputs.versionUscore }}" + $thumb = "${{ secrets.CODESIGN_THUMBPRINT }}" - $appBin = "src/OutlookAddin/bin/Release" - $publishDir = "publish" - $appFilesDir = "$publishDir/Application Files/OutlookAddin_$vUnderscore" + $appBin = "src/OutlookAddin/bin/Release" + $publishDir = "publish" + $appFilesDir = "$publishDir/Application Files/OutlookAddin_$versionUscore" New-Item -ItemType Directory -Force -Path $appFilesDir | Out-Null Copy-Item -Recurse -Force "$appBin/*" $appFilesDir - # Application manifest (.dll.manifest / .vsto). + # Application manifest (.dll.manifest) & "$env:MAGE" -New Application ` - -ToFile "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" ` - -Name "Marcus-Law OutlookAddin" ` - -Version $version ` - -FromDirectory $appFilesDir ` - -TrustLevel FullTrust + -ToFile "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" ` + -Name "Marcus-Law OutlookAddin" ` + -Version $version ` + -FromDirectory $appFilesDir ` + -TrustLevel FullTrust & "$env:MAGE" -Sign "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" ` - -CertHash $thumb ` - -TimeStampUri http://timestamp.digicert.com + -CertHash $thumb ` + -TimeStampUri http://timestamp.digicert.com - # Deployment manifest (.vsto). + # Deployment manifest (.vsto) Copy-Item "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" "$publishDir/OutlookAddin.vsto" & "$env:MAGE" -Update "$publishDir/OutlookAddin.vsto" ` - -AppManifest "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" ` - -ProviderUrl "$env:PROVIDER_URL" ` - -Publisher "Marcus-Law" ` - -Install true + -AppManifest "$appFilesDir/MarcusLaw.OutlookAddin.dll.manifest" ` + -ProviderUrl "$env:PROVIDER_URL" ` + -Publisher "Marcus-Law" ` + -Install true & "$env:MAGE" -Sign "$publishDir/OutlookAddin.vsto" ` - -CertHash $thumb ` - -TimeStampUri http://timestamp.digicert.com + -CertHash $thumb ` + -TimeStampUri http://timestamp.digicert.com - # Mark the .deploy convention so IIS/nginx serves a static URL list. + # Rename payload files to .deploy (ClickOnce convention). Get-ChildItem -Recurse -File "$appFilesDir" | - Where-Object { $_.Extension -notin '.manifest','.deploy' } | - ForEach-Object { Rename-Item -LiteralPath $_.FullName -NewName ($_.Name + '.deploy') } + Where-Object { $_.Extension -notin '.manifest','.deploy' } | + ForEach-Object { Rename-Item -LiteralPath $_.FullName -NewName ($_.Name + '.deploy') } - name: Stage protocol handler shell: pwsh @@ -125,70 +153,95 @@ jobs: $stagingDir = "publish/protocol-handler" New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null Copy-Item -Recurse -Force "src/OutlookAddinProtocolHandler/bin/Release/net48/*" $stagingDir - Copy-Item -Force "tools/install-protocol.ps1" "$stagingDir/install-protocol.ps1" + Copy-Item -Force "tools/install-protocol.ps1" "$stagingDir/install-protocol.ps1" Copy-Item -Force "tools/uninstall-protocol.ps1" "$stagingDir/uninstall-protocol.ps1" - - name: Upload artifact (publish/) + - name: Upload publish artifact uses: actions/upload-artifact@v4 with: - name: outlookaddin-${{ steps.ver.outputs.versionDot }} + name: clickonce-${{ steps.ver.outputs.versionDot }} path: publish/ - - name: Upload to platform.dev - env: - SSH_KEY: ${{ secrets.PLATFORM_DEV_SSH_KEY }} - shell: pwsh + dockerize-and-deploy: + needs: publish-clickonce + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + env: + # Set this Gitea Actions secret AFTER the outlook-addin-cdn Coolify app + # is created. If empty, the trigger step is skipped (image still builds). + COOLIFY_UUID: ${{ secrets.COOLIFY_UUID }} + steps: + - name: Checkout (Dockerfile + nginx.conf) + uses: actions/checkout@v4 + + - name: Compute version from tag + id: ver run: | - if ([string]::IsNullOrWhiteSpace($env:SSH_KEY)) { - Write-Warning "PLATFORM_DEV_SSH_KEY not set — skipping upload" - exit 0 - } - $keyPath = Join-Path $env:TEMP "platform_ssh_key" - [System.IO.File]::WriteAllBytes($keyPath, [System.Convert]::FromBase64String($env:SSH_KEY)) - icacls $keyPath /inheritance:r /grant:r "$env:USERNAME`:F" | Out-Null + TAG="${GITHUB_REF##refs/tags/}" + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Tag $TAG is not in vMAJOR.MINOR.PATCH form" >&2 + exit 1 + fi + VERSION="${TAG#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT - # Requires WSL on the runner; falls back to scp if rsync isn't available. - $remote = "root@$env:REMOTE_HOST:$env:REMOTE_DIR/" - $useWsl = $false - try { wsl --status | Out-Null; $useWsl = $true } catch { $useWsl = $false } + - name: Download publish artifact + uses: actions/download-artifact@v4 + with: + name: clickonce-${{ steps.ver.outputs.version }} + path: cdn/publish/ - if ($useWsl) { - wsl rsync -avz --delete ` - -e "ssh -i /mnt/c/Users/runneradmin/AppData/Local/Temp/platform_ssh_key -o StrictHostKeyChecking=no" ` - ./publish/ ` - $remote - } else { - scp -i $keyPath -o StrictHostKeyChecking=no -r publish/* $remote - } + - name: Login to Gitea Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login ${{ env.REGISTRY }} \ + -u "${{ secrets.REGISTRY_USER }}" --password-stdin - Remove-Item $keyPath -Force + - name: Build and push image + run: | + BASE="${{ env.REGISTRY }}/${{ env.CDN_IMAGE_NAME }}" + docker build \ + -t "${BASE}:latest" \ + -t "${BASE}:${{ steps.ver.outputs.tag }}" \ + -t "${BASE}:build-${{ github.run_number }}" \ + cdn/ + docker push "${BASE}:latest" + docker push "${BASE}:${{ steps.ver.outputs.tag }}" + docker push "${BASE}:build-${{ github.run_number }}" - - name: Notify Mattermost + - name: Trigger Coolify redeploy + if: env.COOLIFY_UUID != '' + run: | + curl -sf \ + "http://coolify:8080/api/v1/deploy?uuid=${{ env.COOLIFY_UUID }}&force=true" \ + -H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" + + - name: Notify Mattermost (success) if: success() - shell: pwsh env: WEBHOOK: ${{ secrets.MATTERMOST_WEBHOOK_RELEASES }} run: | - if ([string]::IsNullOrWhiteSpace($env:WEBHOOK)) { - Write-Warning "MATTERMOST_WEBHOOK_RELEASES not set — skipping notification" - exit 0 + if [ -z "$WEBHOOK" ]; then exit 0; fi + VERSION="${{ steps.ver.outputs.version }}" + curl -sf -X POST -H 'Content-Type: application/json' "$WEBHOOK" -d "$(cat <