Release: weekly deploy

This commit is contained in:
Danila Khodjaef
2026-07-09 13:31:47 +03:00
parent 3810e61bb8
commit f72ddcc3e4
11 changed files with 154 additions and 5 deletions
+8
View File
@@ -0,0 +1,8 @@
this file contains draft for future releases and features
1. Multy language support, russian version at least, search for best implementation, preferably story in db and easily expandable. tool to check and correct translation
2. tool to manage links (influence/influenced by ) import csv's ( define format), edit ,add, delete, visualize, map to pictures/ entities
3. tool to monitor/manage (plan actions) of curator actions, markers to check painting/text ?
4. tool to sync prod /env resources (both ways), db structure, db data, images, users etc
5. curator_audit_log should contain log of actions like fixit, checked, upload etc with details for which entity it was made and details what was the action and outcome
+5 -1
View File
@@ -31,12 +31,16 @@ For a typical weekly release, use the orchestrator instead of running each step
2. Edit [`infra/deploy/devtoprod.config.json`](../infra/deploy/devtoprod.config.json) (gitignored — safe for SMB password):
- `profile`: `full` | `code` | `data` (base step set; individual `steps` overrides win)
- `autoConfirm`: `true` skips restore/image-sync prompts (`CONFIRM_PROD=1`, `-SkipConfirm`)
- `autoStartDevStack`: `true` starts `dev:server` (or `dev:web`) automatically when `validateBuild` finds API down
- `git.message`, `git.branch` — used when `gitCommitPush` is enabled
- `schemaChanged`: `true` enables prod schema migration on `full` profile
- `release.imageTag`: image/version tag shared between Gitea push and runtime (`latest`, `2026-07-09`, `v1.3.0`, etc.)
- `smb.user` / `smb.password` — optional; maps `\\host\share` before image sync
- `backupFile` — optional fixed path; otherwise uses the newest `gallery_dev_data_*.txt` after backup
3. Prerequisites still apply: **`npm run dev:web` running** (Vite `:5173` + API `:3451`), Docker Desktop running, `docker login gitea.mysuperlab.netcraze.pro`, [`infra/docker/.env.prod`](../infra/docker/.env.prod) present.
- Before migration/restore steps, verify prod DB target in `.env.prod`: `DB_NAME=gallery_prod`.
- Set `IMAGE_TAG=<release.imageTag>` in prod env (TrueNAS app vars or `.env.prod`) to control which pushed Gitea image tag is active.
### Run
@@ -64,7 +68,7 @@ npm run devtoprod:release -- -DryRun
| `migrateProdSchema` | Step 4 — `dev:migrate` on `gallery_prod` |
| `restoreProd` | Step 5 — `devtoprod:db:restore` |
| `syncImages` | Step 6 — `devtoprod:images` |
| `dockerPublish` | Step 7 — `prod:docker:publish` |
| `dockerPublish` | Step 7 — `prod:docker:publish -- -Tag <release.imageTag>` |
| `truenasRestartPause` | Step 8 — **manual** pause; script waits for Enter after you restart **gallery-web** |
| `verify` | Step 9 — curl LAN + public `/api/bounds` |
+5
View File
@@ -7,6 +7,11 @@ Gallery uses **one PostgreSQL server** on TrueNAS (`192.168.10.122`) with **two
| **Development** | `gallery_dev` | `https://devgallery.mysuperlab.netcraze.pro` | Dev PC `192.168.10.70:5173` |
| **Production** | `gallery_prod` | `https://gallery.mysuperlab.netcraze.pro` | TrueNAS container `192.168.10.122:5173` |
Version tracking:
- Runtime version endpoint: `/api/version` (reports `app_env`, `app_version`, `image_tag`, `git_sha`, `db_name`)
- Prod image selection: set `IMAGE_TAG` in TrueNAS app env / `infra/docker/.env.prod`
- Release push tag: set `release.imageTag` in `infra/deploy/devtoprod.config.json`
---
## Development-first workflow (default)
Binary file not shown.
@@ -2,6 +2,7 @@
"_comment": "Copy to devtoprod.config.json (gitignored) and edit before running npm run devtoprod:release",
"profile": "full",
"autoConfirm": true,
"autoStartDevStack": true,
"steps": {
"validateBuild": true,
"gitCommitPush": true,
@@ -21,6 +22,9 @@
"stageAll": true
},
"schemaChanged": false,
"release": {
"imageTag": "latest"
},
"backupFile": "",
"smb": {
"host": "192.168.10.122",
+2
View File
@@ -10,6 +10,8 @@ DB_NAME=gallery_prod
PORT=5173
HOST=0.0.0.0
APP_ENV=prod
IMAGE_TAG=latest
PUBLIC_URL=https://gallery.mysuperlab.netcraze.pro
TRUST_PROXY=true
IMAGE_DIR=/app/data/images
+1 -1
View File
@@ -6,7 +6,7 @@ services:
build:
context: ../..
dockerfile: infra/docker/Dockerfile
image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:latest
image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:${IMAGE_TAG:-latest}
container_name: gallery-web
restart: unless-stopped
ports:
+3 -1
View File
@@ -10,7 +10,7 @@
# See infra/docker/DEPLOY-truenas.md and Documentation/environments.md
services:
web:
image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:latest
image: gitea.mysuperlab.netcraze.pro/danilka/gallery-web:${IMAGE_TAG:-latest}
container_name: gallery-web
restart: unless-stopped
pull_policy: always
@@ -18,6 +18,8 @@ services:
- "5173:5173"
environment:
NODE_ENV: production
APP_ENV: "prod"
IMAGE_TAG: "${IMAGE_TAG:-latest}"
PORT: "5173"
HOST: "0.0.0.0"
DB_HOST: "192.168.10.122"
+105 -1
View File
@@ -203,6 +203,31 @@ function Get-LatestDevBackup {
return $latest.FullName
}
function Assert-ProdDbTarget {
$prodEnvPath = Join-Path $RepoRoot 'infra\docker\.env.prod'
if (-not (Test-Path $prodEnvPath)) {
throw "Missing production env file: $prodEnvPath"
}
$prodDbName = $null
foreach ($line in Get-Content -LiteralPath $prodEnvPath) {
if ($line -match '^\s*DB_NAME\s*=\s*(.+?)\s*$') {
$prodDbName = $Matches[1].Trim().Trim('"', "'")
break
}
}
if (-not $prodDbName) {
throw "DB_NAME is not set in $prodEnvPath"
}
if ($prodDbName -ne 'gallery_prod') {
throw "Prod DB target is '$prodDbName' in .env.prod. Set DB_NAME=gallery_prod before dev -> prod migration."
}
Write-Host "Prod DB target confirmed: $prodDbName" -ForegroundColor DarkGray
}
function Get-DevApiPort {
$envFile = Join-Path $RepoRoot '.env'
$port = '3451'
@@ -217,6 +242,72 @@ function Get-DevApiPort {
return $port
}
function Test-TcpPortListening {
param([int]$Port)
$matches = netstat -ano | Select-String ":$Port\s"
foreach ($line in $matches) {
if ($line -match 'LISTENING') {
return $true
}
}
return $false
}
function Start-DevNpmProcess {
param([string]$ScriptName)
$logPath = Join-Path $RepoRoot "infra\deploy\dev-autostart-$ScriptName.log"
Write-Host "Starting background process: npm run $ScriptName (log: $logPath)" -ForegroundColor Yellow
Start-Process -FilePath 'cmd.exe' -ArgumentList @(
'/c',
"npm run $ScriptName > `"$logPath`" 2>&1"
) -WorkingDirectory $RepoRoot -WindowStyle Hidden | Out-Null
}
function Ensure-DevApiReady {
param(
[int]$ApiPort,
[bool]$AutoStart = $true,
[int]$WaitSeconds = 90
)
$apiUrl = "http://127.0.0.1:$ApiPort/api/bounds"
$detail = ''
if (Test-BoundsJson -Url $apiUrl -MaxTimeSeconds 5 -Retries 1 -Detail ([ref]$detail)) {
Write-Host "Dev API already running: $apiUrl"
return $true
}
if (-not $AutoStart) {
return $false
}
$viteUp = Test-TcpPortListening -Port 5173
$apiUp = Test-TcpPortListening -Port $ApiPort
if ($viteUp -and -not $apiUp) {
Write-Host "Vite is running on :5173 but API is down on :$ApiPort." -ForegroundColor Yellow
Start-DevNpmProcess -ScriptName 'dev:server'
} elseif (-not $viteUp -and -not $apiUp) {
Write-Host 'Dev stack is not running.' -ForegroundColor Yellow
Start-DevNpmProcess -ScriptName 'dev:web'
} else {
Write-Host "API port :$ApiPort is listening but /api/bounds is not ready yet. Waiting..." -ForegroundColor Yellow
}
for ($elapsed = 3; $elapsed -le $WaitSeconds; $elapsed += 3) {
Start-Sleep -Seconds 3
if (Test-BoundsJson -Url $apiUrl -MaxTimeSeconds 5 -Retries 1 -Detail ([ref]$detail)) {
Write-Host "Dev API ready: $apiUrl"
return $true
}
Write-Host "Waiting for dev API... (${elapsed}s, $detail)" -ForegroundColor DarkGray
}
return $false
}
function Test-BoundsJson {
param(
[string]$Url,
@@ -312,6 +403,14 @@ if (Get-StepEnabled $steps 'validateBuild') {
# Dev-only endpoints. Do not use verify.lanUrl here (that is prod on TrueNAS).
$devApiPort = Get-DevApiPort
$autoStartDev = if ($null -ne $cfg.autoStartDevStack) { [bool]$cfg.autoStartDevStack } else { $true }
if (-not (Ensure-DevApiReady -ApiPort $devApiPort -AutoStart:$autoStartDev)) {
Write-Host "Dev API check failed on all URLs." -ForegroundColor Red
Write-Host "Start the full dev stack before release: npm run dev:web" -ForegroundColor Yellow
Write-Host "Vite on :5173 alone is not enough - the API must listen on :$devApiPort (PORT in .env)." -ForegroundColor Yellow
return 1
}
$devCandidates = @(
"http://127.0.0.1:$devApiPort/api/bounds"
'http://localhost:5173/api/bounds'
@@ -395,6 +494,7 @@ if (Get-StepEnabled $steps 'backupDev') {
if (Get-StepEnabled $steps 'backupProd') {
$ok = Invoke-Step 'backupProd' {
Assert-ProdDbTarget
$code = Invoke-Npm 'prod:db:backup'
return $code
}
@@ -403,6 +503,7 @@ if (Get-StepEnabled $steps 'backupProd') {
if (Get-StepEnabled $steps 'migrateProdSchema') {
$ok = Invoke-Step 'migrateProdSchema' {
Assert-ProdDbTarget
$env:DB_NAME = 'gallery_prod'
try {
$code = Invoke-Npm 'dev:migrate'
@@ -416,6 +517,7 @@ if (Get-StepEnabled $steps 'migrateProdSchema') {
if (Get-StepEnabled $steps 'restoreProd') {
$ok = Invoke-Step 'restoreProd' {
Assert-ProdDbTarget
if (-not $script:backupFile) {
if ($cfg.backupFile -and $cfg.backupFile.Trim()) {
$script:backupFile = (Resolve-Path (Join-Path $RepoRoot $cfg.backupFile)).Path
@@ -446,7 +548,9 @@ if (Get-StepEnabled $steps 'syncImages') {
if (Get-StepEnabled $steps 'dockerPublish') {
$ok = Invoke-Step 'dockerPublish' {
$code = Invoke-Npm 'prod:docker:publish'
$tag = if ($cfg.release.imageTag) { $cfg.release.imageTag } else { 'latest' }
Write-Host "Docker release tag: $tag"
$code = Invoke-Npm 'prod:docker:publish' @('-Tag', $tag)
return $code
}
if (-not $ok) { Write-ReleaseBanner -Success $false -FailedAt $FailedStep; exit 1 }
+6 -1
View File
@@ -10,7 +10,12 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
const apiPort = process.env.DEV_API_PORT || process.env.PORT || '3451';
const vitePort = process.env.DEV_WEB_PORT || '5173';
const env = { ...process.env, PORT: apiPort };
const env = {
...process.env,
PORT: apiPort,
APP_ENV: process.env.APP_ENV || 'dev',
IMAGE_TAG: process.env.IMAGE_TAG || 'dev-local',
};
function run(label, command, args, cwd) {
const child = spawn(command, args, {
+15
View File
@@ -18,6 +18,10 @@ const app = express();
const HOST = process.env.HOST || '0.0.0.0';
const PORT = Number(process.env.PORT) || 3001;
const PUBLIC_URL = process.env.PUBLIC_URL || '';
const APP_ENV = process.env.APP_ENV || (process.env.DB_NAME === 'gallery_prod' ? 'prod' : 'dev');
const IMAGE_TAG = process.env.IMAGE_TAG || 'local';
const GIT_SHA = process.env.GIT_SHA || '';
const APP_VERSION = process.env.npm_package_version || '0.0.0';
if (process.env.TRUST_PROXY === '1' || process.env.TRUST_PROXY === 'true') {
app.set('trust proxy', 1);
@@ -1039,6 +1043,17 @@ app.get('/api/bounds', async (req, res) => {
}
});
app.get('/api/version', async (_req, res) => {
res.json({
app_env: APP_ENV,
app_version: APP_VERSION,
image_tag: IMAGE_TAG,
git_sha: GIT_SHA,
db_name: process.env.DB_NAME || '',
public_url: PUBLIC_URL || '',
});
});
const CLIENT_DIST = path.join(__dirname, '..', 'client', 'dist');
if (fs.existsSync(CLIENT_DIST)) {
app.use(express.static(CLIENT_DIST));