Skip to content

Commit 6a4ee53

Browse files
baasith6cursoragent
andcommitted
Add Jenkins pipeline and VM deploy script for Azure MVP.
Enables CI, tarball deploy to /opt/onevo/app, and smoke tests from local Jenkins. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e36007d commit 6a4ee53

7 files changed

Lines changed: 353 additions & 34 deletions

File tree

.github/workflows/deploy-mvp.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Deploy MVP (Azure)
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, azure-mvp-deploy]
66
workflow_dispatch:
77

88
concurrency:
@@ -11,6 +11,7 @@ concurrency:
1111

1212
env:
1313
IMAGE_TAG: ${{ github.sha }}
14+
ONEVO_APP_DIR: /opt/onevo/app
1415

1516
jobs:
1617
build-push:
@@ -100,7 +101,8 @@ jobs:
100101
--exclude node_modules \
101102
--exclude dashboard/dist \
102103
--exclude connector/dist/build \
103-
-e ssh ./ "${VM_USER}@${VM_HOST}:/opt/onevo/"
104+
--exclude "installer-site/*.exe" \
105+
-e ssh ./ "${VM_USER}@${VM_HOST}:${ONEVO_APP_DIR}/"
104106
105107
- name: Write production .env and deploy
106108
env:
@@ -110,7 +112,7 @@ jobs:
110112
DASHBOARD_IMAGE: ${{ needs.build-push.outputs.dashboard_image }}
111113
CLOUD_AI_IMAGE: ${{ needs.build-push.outputs.cloud_ai_image }}
112114
run: |
113-
ssh "${VM_USER}@${VM_HOST}" "mkdir -p /opt/onevo/connector/dist"
115+
ssh "${VM_USER}@${VM_HOST}" "mkdir -p ${ONEVO_APP_DIR}/installer-site ${ONEVO_APP_DIR}/connector/dist"
114116
printf '%s\n' "${{ secrets.PRODUCTION_ENV }}" > /tmp/onevo.env
115117
{
116118
echo ""
@@ -121,8 +123,8 @@ jobs:
121123
echo "ACR_USERNAME=${{ secrets.ACR_USERNAME }}"
122124
echo "ACR_PASSWORD=${{ secrets.ACR_PASSWORD }}"
123125
} >> /tmp/onevo.env
124-
scp /tmp/onevo.env "${VM_USER}@${VM_HOST}:/opt/onevo/.env"
125-
ssh "${VM_USER}@${VM_HOST}" "chmod +x /opt/onevo/infra/mvp/deploy.sh && /opt/onevo/infra/mvp/deploy.sh"
126+
scp /tmp/onevo.env "${VM_USER}@${VM_HOST}:${ONEVO_APP_DIR}/.env"
127+
ssh "${VM_USER}@${VM_HOST}" "chmod +x ${ONEVO_APP_DIR}/infra/mvp/deploy.sh && ONEVO_DIR=${ONEVO_APP_DIR} USE_GPU=false DEPLOY_MODE=acr ${ONEVO_APP_DIR}/infra/mvp/deploy.sh"
126128
127129
- name: Smoke test (public API)
128130
env:
@@ -168,7 +170,7 @@ jobs:
168170
pip install -r connector/requirements-build.txt
169171
$url = "${{ secrets.BACKEND_PUBLIC_URL }}"
170172
if (-not $url) { throw "Set BACKEND_PUBLIC_URL secret (https://api.yourdomain.example)" }
171-
./scripts/build-installer.ps1 -BackendUrl $url
173+
./scripts/build-installer.ps1 -BackendUrl $url -AllowHttp
172174
173175
- name: Upload installer artifact
174176
uses: actions/upload-artifact@v4
@@ -188,5 +190,7 @@ jobs:
188190
if (-not $exe) { throw "Installer EXE not found" }
189191
$keyPath = "$env:RUNNER_TEMP/deploy_key"
190192
Set-Content -Path $keyPath -Value $env:VM_SSH_KEY -NoNewline
191-
scp -i $keyPath -o StrictHostKeyChecking=no $exe.FullName "${env:VM_USER}@${env:VM_HOST}:/opt/onevo/connector/dist/"
192-
ssh -i $keyPath -o StrictHostKeyChecking=no "${env:VM_USER}@${env:VM_HOST}" "ls -la /opt/onevo/connector/dist/"
193+
$appDir = "/opt/onevo/app"
194+
scp -i $keyPath -o StrictHostKeyChecking=no $exe.FullName "${env:VM_USER}@${env:VM_HOST}:${appDir}/installer-site/"
195+
scp -i $keyPath -o StrictHostKeyChecking=no $exe.FullName "${env:VM_USER}@${env:VM_HOST}:${appDir}/connector/dist/"
196+
ssh -i $keyPath -o StrictHostKeyChecking=no "${env:VM_USER}@${env:VM_HOST}" "ls -la ${appDir}/installer-site/"

Jenkinsfile

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
pipeline {
2+
agent any
3+
4+
parameters {
5+
string(name: 'VM_HOST', defaultValue: '20.193.69.220', description: 'Azure VM public IP')
6+
string(name: 'VM_USER', defaultValue: 'azureuser', description: 'SSH user on VM')
7+
string(name: 'BACKEND_URL', defaultValue: 'http://20.193.69.220:8081', description: 'Public API URL (installer bake + smoke)')
8+
booleanParam(name: 'SKIP_INSTALLER', defaultValue: false, description: 'Skip Windows connector EXE build/upload')
9+
booleanParam(name: 'SKIP_CI', defaultValue: false, description: 'Skip CI build/test stages')
10+
booleanParam(name: 'USE_GPU', defaultValue: false, description: 'Include docker-compose.gpu.yml (requires NVIDIA on VM)')
11+
}
12+
13+
environment {
14+
VM_APP_DIR = '/opt/onevo/app'
15+
}
16+
17+
stages {
18+
stage('CI') {
19+
when { expression { !params.SKIP_CI } }
20+
parallel {
21+
stage('Backend') {
22+
steps {
23+
dir('backend') {
24+
bat 'dotnet restore Onevo.Api.csproj'
25+
bat 'dotnet build Onevo.Api.csproj -c Release --no-restore'
26+
}
27+
}
28+
}
29+
stage('Dashboard') {
30+
steps {
31+
dir('dashboard') {
32+
bat 'npm ci'
33+
bat 'npm run build'
34+
}
35+
}
36+
}
37+
stage('Connector tests') {
38+
steps {
39+
dir('connector') {
40+
bat 'pip install -r requirements.txt pytest'
41+
bat 'set PYTHONPATH=.&& python -m pytest tests/ -q'
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
stage('Deploy to VM') {
49+
steps {
50+
script {
51+
def extra = ''
52+
if (params.SKIP_INSTALLER) { extra += ' -SkipInstaller' }
53+
if (params.USE_GPU) { extra += ' -UseGpu' }
54+
withCredentials([sshUserPrivateKey(
55+
credentialsId: 'onevo-vm-ssh-key',
56+
keyFileVariable: 'SSH_KEY',
57+
usernameVariable: 'SSH_USER'
58+
)]) {
59+
bat """
60+
powershell -ExecutionPolicy Bypass -File scripts/deploy-vm.ps1 ^
61+
-VmHost ${params.VM_HOST} ^
62+
-VmUser ${params.VM_USER} ^
63+
-BackendUrl ${params.BACKEND_URL} ^
64+
-SshKeyPath %SSH_KEY%${extra}
65+
"""
66+
}
67+
}
68+
}
69+
}
70+
71+
stage('Smoke test') {
72+
steps {
73+
bat """
74+
curl -sf ${params.BACKEND_URL}/api/health
75+
curl -sf -o NUL -w "Dashboard HTTP %%{http_code}\\n" http://${params.VM_HOST}:4200/
76+
"""
77+
}
78+
}
79+
}
80+
81+
post {
82+
success {
83+
echo "ONEVO deploy succeeded — dashboard http://${params.VM_HOST}:4200"
84+
}
85+
failure {
86+
echo 'Deploy failed — check Jenkins console and VM docker compose logs.'
87+
}
88+
}
89+
}

docs/AZURE_MVP_DEPLOY.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Deploy ONEVO to a **single GPU Azure VM** with **Docker Compose**, built and rel
88
|-----------|----------------|
99
| Backend, dashboard, cloud-ai, Postgres, Redis, MinIO | Azure GPU VM (Docker Compose) |
1010
| Windows connector | **Shop PCs** — downloaded from dashboard after login |
11-
| CI/CD | GitHub Actions → ACR → SSH deploy to VM |
11+
| CI/CD | GitHub Actions or **Jenkins** → ACR or VM build → SSH deploy to VM |
1212

13-
Shop staff download `ONEVO-Connector-Setup-*.exe` from **Get started / Admin / Setup** in the dashboard. The backend serves the file from `/opt/onevo/connector/dist/` on the VM (mounted into the backend container). The connector service itself does **not** run in Azure.
13+
Shop staff download `ONEVO-Connector-Setup-*.exe` from **Get started / Admin / Setup** in the dashboard. The backend serves the file from `/opt/onevo/app/installer-site/` on the VM (mounted into the backend container). The connector service itself does **not** run in Azure.
1414

1515
## 1. Provision Azure (one time)
1616

@@ -33,8 +33,8 @@ SSH to the VM and bootstrap:
3333

3434
```bash
3535
ssh azureuser@<VM_PUBLIC_IP>
36-
git clone <your-repo-url> /opt/onevo
37-
cd /opt/onevo
36+
git clone <your-repo-url> /opt/onevo/app
37+
cd /opt/onevo/app
3838
sudo ACR_LOGIN_SERVER=<acr>.azurecr.io bash infra/mvp/vm-setup.sh
3939
```
4040

@@ -47,8 +47,8 @@ sudo certbot --nginx -d app.yourdomain.example -d api.yourdomain.example
4747
Copy and fill secrets:
4848

4949
```bash
50-
cp infra/mvp/.env.production.example /opt/onevo/.env
51-
nano /opt/onevo/.env
50+
cp infra/mvp/.env.production.example /opt/onevo/app/.env
51+
nano /opt/onevo/app/.env
5252
```
5353

5454
## 2. GitHub secrets
@@ -128,7 +128,7 @@ Manual deploy: Actions → **Deploy MVP (Azure)** → Run workflow.
128128
|-------|-----|
129129
| GPU quota denied | Use CPU VM or request quota increase; set `CLOUD_AI_DEVICE=cpu` in `.env` |
130130
| ACR pull 401 on VM | Check `ACR_*` in `.env`; run `docker login` manually on VM |
131-
| Installer 404 | Ensure `build-installer` job succeeded and EXE exists in `/opt/onevo/connector/dist/` |
131+
| Installer 404 | Ensure `build-installer` job succeeded and EXE exists in `/opt/onevo/app/installer-site/` |
132132
| CORS errors | Set `CORS_ORIGINS=https://app.yourdomain.example` in `.env` |
133133
| Deploy SSH fails | Verify `VM_SSH_KEY`, NSG allows SSH from GitHub Actions IPs (or use self-hosted runner in same VNet) |
134134
| Clip upload timeout (`:9000`) | NSG must allow **9000**; set `S3_PUBLIC_ENDPOINT=http://<VM_IP>:9000` in `.env`; test `curl http://<VM_IP>:9000/minio/health/live` from shop PC |
@@ -140,7 +140,10 @@ Manual deploy: Actions → **Deploy MVP (Azure)** → Run workflow.
140140
|------|---------|
141141
| [`infra/mvp/provision-azure.sh`](../infra/mvp/provision-azure.sh) | Create RG, ACR, VM |
142142
| [`infra/mvp/vm-setup.sh`](../infra/mvp/vm-setup.sh) | Docker, NVIDIA toolkit, nginx on VM |
143-
| [`infra/mvp/deploy.sh`](../infra/mvp/deploy.sh) | Pull ACR images and restart compose |
143+
| [`infra/mvp/deploy.sh`](../infra/mvp/deploy.sh) | Pull ACR images or local build; restart compose |
144+
| [`scripts/deploy-vm.ps1`](../scripts/deploy-vm.ps1) | Jenkins / manual deploy from Windows |
145+
| [`Jenkinsfile`](../Jenkinsfile) | Jenkins pipeline definition |
146+
| [`docs/JENKINS_DEPLOY.md`](JENKINS_DEPLOY.md) | Jenkins setup on Windows |
144147
| [`infra/mvp/nginx-host.conf`](../infra/mvp/nginx-host.conf) | Host TLS reverse proxy template |
145148
| [`infra/mvp/.env.production.example`](../infra/mvp/.env.production.example) | Production env template |
146149
| [`docker-compose.acr.yml`](../docker-compose.acr.yml) | Use pre-built images from ACR |

docs/JENKINS_DEPLOY.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Jenkins CI/CD on Windows
2+
3+
Deploy ONEVO to the Azure MVP VM from your local Windows PC using the root [`Jenkinsfile`](../Jenkinsfile) and [`scripts/deploy-vm.ps1`](../scripts/deploy-vm.ps1).
4+
5+
## Why Jenkins here
6+
7+
- Builds the **Windows connector installer** on the same machine (Inno Setup + PyInstaller).
8+
- SSH/SCP to the VM already works from your dev PC.
9+
- Avoids GitHub Actions secret/path drift while the pilot is active.
10+
11+
## Prerequisites
12+
13+
1. [Jenkins LTS](https://www.jenkins.io/download/) installed on Windows.
14+
2. **Git** and **OpenSSH client** (Windows 10+ optional feature or Git for Windows).
15+
3. **.NET 8 SDK**, **Node.js 20**, **Python 3.11+** on the Jenkins agent (same PC).
16+
4. **Inno Setup 6** + PyInstaller deps for connector installer (see [`connector/installer/INSTALL.md`](../connector/installer/INSTALL.md)).
17+
5. SSH private key that can log in as `azureuser@20.193.69.220`.
18+
19+
## Jenkins plugins
20+
21+
- Pipeline
22+
- Git
23+
- Credentials Binding
24+
- SSH Agent (optional)
25+
26+
## Credentials (Jenkins → Manage Credentials)
27+
28+
| ID | Type | Value |
29+
|----|------|--------|
30+
| `onevo-vm-ssh-key` | SSH Username with private key | User `azureuser`, paste private key |
31+
32+
## Create the pipeline job
33+
34+
1. **New Item** → name `onevo-deploy`**Pipeline**.
35+
2. **Pipeline** → Definition: **Pipeline script from SCM**.
36+
3. SCM: **Git**, repository URL, branch `azure-mvp-deploy` or `main`.
37+
4. Script Path: `Jenkinsfile`.
38+
5. Save → **Build with Parameters**.
39+
40+
### Parameters (defaults)
41+
42+
| Parameter | Default |
43+
|-----------|---------|
44+
| `VM_HOST` | `20.193.69.220` |
45+
| `VM_USER` | `azureuser` |
46+
| `BACKEND_URL` | `http://20.193.69.220:8081` |
47+
| `SKIP_INSTALLER` | false |
48+
| `SKIP_CI` | false |
49+
| `USE_GPU` | false (CPU VM) |
50+
51+
## Manual deploy (without Jenkins)
52+
53+
From repo root in PowerShell:
54+
55+
```powershell
56+
powershell -ExecutionPolicy Bypass -File scripts/deploy-vm.ps1 `
57+
-VmHost 20.193.69.220 `
58+
-BackendUrl http://20.193.69.220:8081
59+
```
60+
61+
Skip installer rebuild:
62+
63+
```powershell
64+
powershell -ExecutionPolicy Bypass -File scripts/deploy-vm.ps1 -SkipInstaller
65+
```
66+
67+
## Auto-trigger on git push
68+
69+
**Option A — Poll SCM** (simplest): In job config, **Build Triggers****Poll SCM**`H/5 * * * *` (every 5 minutes).
70+
71+
**Option B — GitHub webhook**: Install **GitHub plugin**, add webhook pointing to `http://<your-jenkins>:8080/github-webhook/`.
72+
73+
## Pipeline stages
74+
75+
1. **CI**`dotnet build`, `npm run build`, `pytest` (parallel).
76+
2. **Deploy to VM** — tarball → SCP → extract under `/opt/onevo/app``docker compose build` + `up`.
77+
3. **Smoke test**`GET /api/health` and dashboard HTTP 200.
78+
79+
## VM layout (canonical)
80+
81+
| Path | Purpose |
82+
|------|---------|
83+
| `/opt/onevo/app/` | Git repo + docker-compose project |
84+
| `/opt/onevo/app/.env` | Production secrets |
85+
| `/opt/onevo/app/installer-site/` | Connector EXE served by backend |
86+
87+
## Troubleshooting
88+
89+
| Issue | Fix |
90+
|-------|-----|
91+
| SSH permission denied | Check `onevo-vm-ssh-key` credential; test `ssh azureuser@20.193.69.220` from Jenkins service account |
92+
| GPU compose error | Keep `USE_GPU=false` on CPU VM |
93+
| Installer build fails | Install Inno Setup; run `scripts/build-installer.ps1` manually once |
94+
| Backend unhealthy after deploy | SSH to VM: `cd /opt/onevo/app && docker compose logs backend --tail 50` |
95+
96+
## Related
97+
98+
- [`docs/AZURE_MVP_DEPLOY.md`](AZURE_MVP_DEPLOY.md) — Azure + GitHub Actions
99+
- [`infra/mvp/deploy.sh`](../infra/mvp/deploy.sh) — VM-side deploy (ACR pull or local build)

infra/mvp/PROVISIONED.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ Also create `AZURE_CREDENTIALS` service principal JSON (see [AZURE_MVP_DEPLOY.md
5656

5757
```bash
5858
ssh azureuser@20.193.69.220
59-
git clone <your-github-repo-url> /opt/onevo
60-
cd /opt/onevo
59+
git clone <your-github-repo-url> /opt/onevo/app
60+
cd /opt/onevo/app
6161
sudo ACR_LOGIN_SERVER=onevoacrmvp.azurecr.io bash infra/mvp/vm-setup.sh
62-
cp infra/mvp/.env.production.example /opt/onevo/.env
62+
cp infra/mvp/.env.production.example /opt/onevo/app/.env
6363
# Edit .env — set CLOUD_AI_DEVICE=cpu, passwords, JWT, SMTP
6464
```
6565

@@ -73,4 +73,4 @@ Azure portal: [Quota increase](https://aka.ms/ProdportalCRP/#blade/Microsoft_Azu
7373

7474
## Installer download
7575

76-
Unchanged: shops download the Windows EXE from the **dashboard** after login. The deploy pipeline copies the built EXE to `/opt/onevo/connector/dist/` on this VM.
76+
Unchanged: shops download the Windows EXE from the **dashboard** after login. The deploy pipeline copies the built EXE to `/opt/onevo/app/installer-site/` on this VM (mounted as `/app/connector-dist` in the backend container).

infra/mvp/deploy.sh

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
#!/usr/bin/env bash
2-
# ONEVO MVP — pull ACR images and restart the stack on the GPU VM.
3-
# Invoked by GitHub Actions over SSH or manually on the VM.
2+
# ONEVO MVP — pull ACR images and restart the stack on the Azure VM.
3+
# Invoked by GitHub Actions, Jenkins, or manually on the VM.
4+
#
5+
# Env:
6+
# ONEVO_DIR — app root (default /opt/onevo/app)
7+
# USE_GPU — "true" to include docker-compose.gpu.yml (default false on CPU VM)
8+
# DEPLOY_MODE — "acr" (pull pre-built images) or "local" (build on VM)
49
set -euo pipefail
510

6-
ONEVO_DIR="${ONEVO_DIR:-/opt/onevo}"
7-
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.gpu.yml -f docker-compose.acr.yml"
11+
ONEVO_DIR="${ONEVO_DIR:-/opt/onevo/app}"
12+
DEPLOY_MODE="${DEPLOY_MODE:-acr}"
13+
14+
COMPOSE_FILES="-f docker-compose.yml -f docker-compose.prod.yml"
15+
if [[ "${USE_GPU:-false}" == "true" ]]; then
16+
COMPOSE_FILES+=" -f docker-compose.gpu.yml"
17+
fi
18+
if [[ "$DEPLOY_MODE" == "acr" ]]; then
19+
COMPOSE_FILES+=" -f docker-compose.acr.yml"
20+
fi
821

922
cd "$ONEVO_DIR"
1023

@@ -18,19 +31,27 @@ set -a
1831
source .env
1932
set +a
2033

21-
: "${ONEVO_BACKEND_IMAGE:?Set ONEVO_BACKEND_IMAGE}"
22-
: "${ONEVO_DASHBOARD_IMAGE:?Set ONEVO_DASHBOARD_IMAGE}"
23-
: "${ONEVO_CLOUD_AI_IMAGE:?Set ONEVO_CLOUD_AI_IMAGE}"
34+
if [[ "$DEPLOY_MODE" == "acr" ]]; then
35+
: "${ONEVO_BACKEND_IMAGE:?Set ONEVO_BACKEND_IMAGE}"
36+
: "${ONEVO_DASHBOARD_IMAGE:?Set ONEVO_DASHBOARD_IMAGE}"
37+
: "${ONEVO_CLOUD_AI_IMAGE:?Set ONEVO_CLOUD_AI_IMAGE}"
2438

25-
if [[ -n "${ACR_LOGIN_SERVER:-}" && -n "${ACR_USERNAME:-}" && -n "${ACR_PASSWORD:-}" ]]; then
26-
echo "$ACR_PASSWORD" | docker login "$ACR_LOGIN_SERVER" -u "$ACR_USERNAME" --password-stdin
27-
fi
39+
if [[ -n "${ACR_LOGIN_SERVER:-}" && -n "${ACR_USERNAME:-}" && -n "${ACR_PASSWORD:-}" ]]; then
40+
echo "$ACR_PASSWORD" | docker login "$ACR_LOGIN_SERVER" -u "$ACR_USERNAME" --password-stdin
41+
fi
42+
43+
echo "==> Pulling images from ACR..."
44+
docker compose $COMPOSE_FILES pull backend dashboard cloud-ai
2845

29-
echo "==> Pulling images..."
30-
docker compose $COMPOSE_FILES pull backend dashboard cloud-ai
46+
echo "==> Starting stack (ACR images)..."
47+
docker compose $COMPOSE_FILES up -d --no-build --remove-orphans
48+
else
49+
echo "==> Building images on VM..."
50+
docker compose $COMPOSE_FILES build backend dashboard cloud-ai
3151

32-
echo "==> Starting stack..."
33-
docker compose $COMPOSE_FILES up -d --no-build --remove-orphans
52+
echo "==> Starting stack (local build)..."
53+
docker compose $COMPOSE_FILES up -d --remove-orphans
54+
fi
3455

3556
echo "==> Waiting for backend health..."
3657
for i in $(seq 1 30); do

0 commit comments

Comments
 (0)