feat(ci): CI/CD container delivery for ClickOnce artifacts
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:<version> + :latest to
gitea.dev.marcus-law.co.il, then trigger Coolify
redeploy of app awdlvjlozz0dvn6kjlme9rd7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+110
-57
@@ -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'
|
||||
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
|
||||
@@ -58,14 +60,40 @@ jobs:
|
||||
"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: |
|
||||
@@ -80,17 +108,17 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = "${{ steps.ver.outputs.version }}"
|
||||
$vUnderscore = "${{ steps.ver.outputs.versionUscore }}"
|
||||
$versionUscore = "${{ steps.ver.outputs.versionUscore }}"
|
||||
$thumb = "${{ secrets.CODESIGN_THUMBPRINT }}"
|
||||
|
||||
$appBin = "src/OutlookAddin/bin/Release"
|
||||
$publishDir = "publish"
|
||||
$appFilesDir = "$publishDir/Application Files/OutlookAddin_$vUnderscore"
|
||||
$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" `
|
||||
@@ -102,7 +130,7 @@ jobs:
|
||||
-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" `
|
||||
@@ -114,7 +142,7 @@ jobs:
|
||||
-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') }
|
||||
@@ -128,67 +156,92 @@ jobs:
|
||||
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
|
||||
dockerize-and-deploy:
|
||||
needs: publish-clickonce
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.PLATFORM_DEV_SSH_KEY }}
|
||||
shell: pwsh
|
||||
# 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 <<EOF
|
||||
{
|
||||
"text": ":package: **OutlookAddin v$VERSION** שוחרר — https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto",
|
||||
"channel": "git-verelasim"
|
||||
}
|
||||
$version = "${{ steps.ver.outputs.versionDot }}"
|
||||
$payload = @{
|
||||
text = ":package: OutlookAddin **v$version** משוחרר — https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto"
|
||||
channel = "git-verelasim"
|
||||
} | ConvertTo-Json -Compress
|
||||
Invoke-RestMethod -Uri $env:WEBHOOK -Method Post -Body $payload -ContentType "application/json"
|
||||
EOF
|
||||
)"
|
||||
|
||||
- name: Notify Mattermost on failure
|
||||
- name: Notify Mattermost (failure)
|
||||
if: failure()
|
||||
shell: pwsh
|
||||
env:
|
||||
WEBHOOK: ${{ secrets.MATTERMOST_WEBHOOK_RELEASES }}
|
||||
run: |
|
||||
if ([string]::IsNullOrWhiteSpace($env:WEBHOOK)) { exit 0 }
|
||||
$payload = @{
|
||||
text = ":warning: OutlookAddin publish ל-${{ github.ref_name }} נכשל — ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
channel = "git-verelasim"
|
||||
} | ConvertTo-Json -Compress
|
||||
Invoke-RestMethod -Uri $env:WEBHOOK -Method Post -Body $payload -ContentType "application/json"
|
||||
if [ -z "$WEBHOOK" ]; then exit 0; fi
|
||||
curl -sf -X POST -H 'Content-Type: application/json' "$WEBHOOK" -d "$(cat <<EOF
|
||||
{
|
||||
"text": ":warning: OutlookAddin publish ל-${{ github.ref_name }} נכשל — ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
||||
"channel": "git-verelasim"
|
||||
}
|
||||
EOF
|
||||
)"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# outlook-addin-cdn — serves ClickOnce artifacts at platform.dev.marcus-law.co.il/outlook-addin/*
|
||||
#
|
||||
# The publish/ directory (produced by the Windows job) is copied into the image
|
||||
# along with a custom nginx.conf that maps the ClickOnce-specific MIME types.
|
||||
# Each release tag produces a versioned image (e.g. :v1.0.0) + :latest.
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
# Drop the default site config; our nginx.conf is the only one.
|
||||
RUN rm -f /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/outlook-addin.conf
|
||||
# Coolify adds a StripPrefix middleware for the /outlook-addin path, so nginx
|
||||
# sees requests at the root (/OutlookAddin.vsto, etc) -- copy files to the
|
||||
# nginx root accordingly.
|
||||
COPY publish/ /usr/share/nginx/html/
|
||||
|
||||
# nginx runs as non-root by default in the alpine image; ensure files readable.
|
||||
RUN chmod -R a+rX /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget -qO- http://localhost/health || exit 1
|
||||
@@ -0,0 +1,44 @@
|
||||
# nginx config for outlook-addin-cdn.
|
||||
#
|
||||
# Routing model: Traefik forwards requests to platform.dev.marcus-law.co.il/outlook-addin/*
|
||||
# and Coolify adds a StripPrefix middleware that strips /outlook-addin. nginx
|
||||
# therefore sees requests at the root path (e.g. /OutlookAddin.vsto).
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# ClickOnce MIME types. Without these, .vsto/.application/.manifest/.deploy
|
||||
# are served as text/plain and the bootstrapper refuses to install.
|
||||
types {
|
||||
application/x-ms-application application;
|
||||
application/x-ms-manifest manifest;
|
||||
application/octet-stream vsto;
|
||||
application/octet-stream deploy;
|
||||
}
|
||||
|
||||
# ClickOnce checks for updates on every Outlook start; never serve stale
|
||||
# manifests from caches.
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
|
||||
# Health endpoint for Coolify/Docker healthcheck (path is checked before
|
||||
# StripPrefix since the healthcheck runs inside the container on port 80).
|
||||
location = /health {
|
||||
access_log off;
|
||||
return 200 "ok";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Never list directories -- that would expose the internal Application Files
|
||||
# layout to anyone browsing.
|
||||
autoindex off;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user