Release: weekly deploy
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
@@ -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):
|
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)
|
- `profile`: `full` | `code` | `data` (base step set; individual `steps` overrides win)
|
||||||
- `autoConfirm`: `true` skips restore/image-sync prompts (`CONFIRM_PROD=1`, `-SkipConfirm`)
|
- `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
|
- `git.message`, `git.branch` — used when `gitCommitPush` is enabled
|
||||||
- `schemaChanged`: `true` enables prod schema migration on `full` profile
|
- `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
|
- `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
|
- `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.
|
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
|
### Run
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ npm run devtoprod:release -- -DryRun
|
|||||||
| `migrateProdSchema` | Step 4 — `dev:migrate` on `gallery_prod` |
|
| `migrateProdSchema` | Step 4 — `dev:migrate` on `gallery_prod` |
|
||||||
| `restoreProd` | Step 5 — `devtoprod:db:restore` |
|
| `restoreProd` | Step 5 — `devtoprod:db:restore` |
|
||||||
| `syncImages` | Step 6 — `devtoprod:images` |
|
| `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** |
|
| `truenasRestartPause` | Step 8 — **manual** pause; script waits for Enter after you restart **gallery-web** |
|
||||||
| `verify` | Step 9 — curl LAN + public `/api/bounds` |
|
| `verify` | Step 9 — curl LAN + public `/api/bounds` |
|
||||||
|
|
||||||
|
|||||||
@@ -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` |
|
| **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` |
|
| **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)
|
## 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",
|
"_comment": "Copy to devtoprod.config.json (gitignored) and edit before running npm run devtoprod:release",
|
||||||
"profile": "full",
|
"profile": "full",
|
||||||
"autoConfirm": true,
|
"autoConfirm": true,
|
||||||
|
"autoStartDevStack": true,
|
||||||
"steps": {
|
"steps": {
|
||||||
"validateBuild": true,
|
"validateBuild": true,
|
||||||
"gitCommitPush": true,
|
"gitCommitPush": true,
|
||||||
@@ -21,6 +22,9 @@
|
|||||||
"stageAll": true
|
"stageAll": true
|
||||||
},
|
},
|
||||||
"schemaChanged": false,
|
"schemaChanged": false,
|
||||||
|
"release": {
|
||||||
|
"imageTag": "latest"
|
||||||
|
},
|
||||||
"backupFile": "",
|
"backupFile": "",
|
||||||
"smb": {
|
"smb": {
|
||||||
"host": "192.168.10.122",
|
"host": "192.168.10.122",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ DB_NAME=gallery_prod
|
|||||||
|
|
||||||
PORT=5173
|
PORT=5173
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
|
APP_ENV=prod
|
||||||
|
IMAGE_TAG=latest
|
||||||
PUBLIC_URL=https://gallery.mysuperlab.netcraze.pro
|
PUBLIC_URL=https://gallery.mysuperlab.netcraze.pro
|
||||||
TRUST_PROXY=true
|
TRUST_PROXY=true
|
||||||
IMAGE_DIR=/app/data/images
|
IMAGE_DIR=/app/data/images
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
dockerfile: infra/docker/Dockerfile
|
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
|
container_name: gallery-web
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
# See infra/docker/DEPLOY-truenas.md and Documentation/environments.md
|
# See infra/docker/DEPLOY-truenas.md and Documentation/environments.md
|
||||||
services:
|
services:
|
||||||
web:
|
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
|
container_name: gallery-web
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
@@ -18,6 +18,8 @@ services:
|
|||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
APP_ENV: "prod"
|
||||||
|
IMAGE_TAG: "${IMAGE_TAG:-latest}"
|
||||||
PORT: "5173"
|
PORT: "5173"
|
||||||
HOST: "0.0.0.0"
|
HOST: "0.0.0.0"
|
||||||
DB_HOST: "192.168.10.122"
|
DB_HOST: "192.168.10.122"
|
||||||
|
|||||||
@@ -203,6 +203,31 @@ function Get-LatestDevBackup {
|
|||||||
return $latest.FullName
|
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 {
|
function Get-DevApiPort {
|
||||||
$envFile = Join-Path $RepoRoot '.env'
|
$envFile = Join-Path $RepoRoot '.env'
|
||||||
$port = '3451'
|
$port = '3451'
|
||||||
@@ -217,6 +242,72 @@ function Get-DevApiPort {
|
|||||||
return $port
|
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 {
|
function Test-BoundsJson {
|
||||||
param(
|
param(
|
||||||
[string]$Url,
|
[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).
|
# Dev-only endpoints. Do not use verify.lanUrl here (that is prod on TrueNAS).
|
||||||
$devApiPort = Get-DevApiPort
|
$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 = @(
|
$devCandidates = @(
|
||||||
"http://127.0.0.1:$devApiPort/api/bounds"
|
"http://127.0.0.1:$devApiPort/api/bounds"
|
||||||
'http://localhost:5173/api/bounds'
|
'http://localhost:5173/api/bounds'
|
||||||
@@ -395,6 +494,7 @@ if (Get-StepEnabled $steps 'backupDev') {
|
|||||||
|
|
||||||
if (Get-StepEnabled $steps 'backupProd') {
|
if (Get-StepEnabled $steps 'backupProd') {
|
||||||
$ok = Invoke-Step 'backupProd' {
|
$ok = Invoke-Step 'backupProd' {
|
||||||
|
Assert-ProdDbTarget
|
||||||
$code = Invoke-Npm 'prod:db:backup'
|
$code = Invoke-Npm 'prod:db:backup'
|
||||||
return $code
|
return $code
|
||||||
}
|
}
|
||||||
@@ -403,6 +503,7 @@ if (Get-StepEnabled $steps 'backupProd') {
|
|||||||
|
|
||||||
if (Get-StepEnabled $steps 'migrateProdSchema') {
|
if (Get-StepEnabled $steps 'migrateProdSchema') {
|
||||||
$ok = Invoke-Step 'migrateProdSchema' {
|
$ok = Invoke-Step 'migrateProdSchema' {
|
||||||
|
Assert-ProdDbTarget
|
||||||
$env:DB_NAME = 'gallery_prod'
|
$env:DB_NAME = 'gallery_prod'
|
||||||
try {
|
try {
|
||||||
$code = Invoke-Npm 'dev:migrate'
|
$code = Invoke-Npm 'dev:migrate'
|
||||||
@@ -416,6 +517,7 @@ if (Get-StepEnabled $steps 'migrateProdSchema') {
|
|||||||
|
|
||||||
if (Get-StepEnabled $steps 'restoreProd') {
|
if (Get-StepEnabled $steps 'restoreProd') {
|
||||||
$ok = Invoke-Step 'restoreProd' {
|
$ok = Invoke-Step 'restoreProd' {
|
||||||
|
Assert-ProdDbTarget
|
||||||
if (-not $script:backupFile) {
|
if (-not $script:backupFile) {
|
||||||
if ($cfg.backupFile -and $cfg.backupFile.Trim()) {
|
if ($cfg.backupFile -and $cfg.backupFile.Trim()) {
|
||||||
$script:backupFile = (Resolve-Path (Join-Path $RepoRoot $cfg.backupFile)).Path
|
$script:backupFile = (Resolve-Path (Join-Path $RepoRoot $cfg.backupFile)).Path
|
||||||
@@ -446,7 +548,9 @@ if (Get-StepEnabled $steps 'syncImages') {
|
|||||||
|
|
||||||
if (Get-StepEnabled $steps 'dockerPublish') {
|
if (Get-StepEnabled $steps 'dockerPublish') {
|
||||||
$ok = Invoke-Step '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
|
return $code
|
||||||
}
|
}
|
||||||
if (-not $ok) { Write-ReleaseBanner -Success $false -FailedAt $FailedStep; exit 1 }
|
if (-not $ok) { Write-ReleaseBanner -Success $false -FailedAt $FailedStep; exit 1 }
|
||||||
|
|||||||
+6
-1
@@ -10,7 +10,12 @@ require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|||||||
const apiPort = process.env.DEV_API_PORT || process.env.PORT || '3451';
|
const apiPort = process.env.DEV_API_PORT || process.env.PORT || '3451';
|
||||||
const vitePort = process.env.DEV_WEB_PORT || '5173';
|
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) {
|
function run(label, command, args, cwd) {
|
||||||
const child = spawn(command, args, {
|
const child = spawn(command, args, {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const app = express();
|
|||||||
const HOST = process.env.HOST || '0.0.0.0';
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
const PORT = Number(process.env.PORT) || 3001;
|
const PORT = Number(process.env.PORT) || 3001;
|
||||||
const PUBLIC_URL = process.env.PUBLIC_URL || '';
|
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') {
|
if (process.env.TRUST_PROXY === '1' || process.env.TRUST_PROXY === 'true') {
|
||||||
app.set('trust proxy', 1);
|
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');
|
const CLIENT_DIST = path.join(__dirname, '..', 'client', 'dist');
|
||||||
if (fs.existsSync(CLIENT_DIST)) {
|
if (fs.existsSync(CLIENT_DIST)) {
|
||||||
app.use(express.static(CLIENT_DIST));
|
app.use(express.static(CLIENT_DIST));
|
||||||
|
|||||||
Reference in New Issue
Block a user