Fix Windows/WSL production deploy hang and permissions.
Skip slow Git worktree walks on /mnt, run Vite on Windows npm, reuse Windows SSH keys, and stop rewriting the klevze-owned public app symlink that skinbase cannot replace.
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
/.deploy
|
/.deploy
|
||||||
/.zed
|
/.zed
|
||||||
/auth.json
|
/auth.json
|
||||||
|
/build-info.json
|
||||||
/node_modules
|
/node_modules
|
||||||
/public/build
|
/public/build
|
||||||
/public/hot
|
/public/hot
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
/storage/*.tar.xz
|
/storage/*.tar.xz
|
||||||
/storage/*.tar
|
/storage/*.tar
|
||||||
/storage/*.tgz
|
/storage/*.tgz
|
||||||
|
/var/deploy/
|
||||||
/vendor
|
/vendor
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
rem Convenience alias so "deploy" works from Command Prompt as deploy.bat
|
||||||
|
call "%~dp0deploy.cmd" %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions
|
||||||
|
|
||||||
|
rem Canonical Windows entrypoint for production deploy.
|
||||||
|
rem Vite/npm run on Windows (this process). rsync/ssh still run in WSL bash.
|
||||||
|
rem This distro cannot execute Windows PE binaries from Linux (Exec format error),
|
||||||
|
rem so powershell.exe/npm.cmd cannot be launched from inside WSL.
|
||||||
|
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%"
|
||||||
|
|
||||||
|
where wsl.exe >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: wsl.exe was not found on PATH.
|
||||||
|
echo Install WSL or run from an existing WSL shell: bash deploy.sh
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Local WSL is Ubuntu. Remote production server is Debian 13 (unrelated to this choice).
|
||||||
|
rem Override only with a non-empty DEPLOY_WSL_DISTRO.
|
||||||
|
set "DEPLOY_WSL_DISTRO_EFFECTIVE=Ubuntu"
|
||||||
|
if defined DEPLOY_WSL_DISTRO if not "%DEPLOY_WSL_DISTRO%"=="" set "DEPLOY_WSL_DISTRO_EFFECTIVE=%DEPLOY_WSL_DISTRO%"
|
||||||
|
|
||||||
|
set "SKIP_WINDOWS_BUILD=0"
|
||||||
|
set "SHOW_HELP=0"
|
||||||
|
for %%A in (%*) do (
|
||||||
|
if /I "%%~A"=="--skip-build" set "SKIP_WINDOWS_BUILD=1"
|
||||||
|
if /I "%%~A"=="--help" set "SHOW_HELP=1"
|
||||||
|
if /I "%%~A"=="-h" set "SHOW_HELP=1"
|
||||||
|
)
|
||||||
|
if defined LOCAL_BUILD_COMMAND set "SKIP_WINDOWS_BUILD=1"
|
||||||
|
|
||||||
|
set "WINDOWS_FRONTEND_BUILT=0"
|
||||||
|
if "%SHOW_HELP%"=="1" goto run_wsl
|
||||||
|
if "%SKIP_WINDOWS_BUILD%"=="1" goto run_wsl
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo Building frontend assets with Windows npm.cmd...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
pushd "%ROOT%"
|
||||||
|
where npm.cmd >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: npm.cmd was not found on PATH.
|
||||||
|
echo Install Node.js or add Laragon's node directory to PATH, then retry.
|
||||||
|
popd
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
call npm.cmd run build
|
||||||
|
set "NPM_EXIT=%ERRORLEVEL%"
|
||||||
|
popd
|
||||||
|
if not "%NPM_EXIT%"=="0" (
|
||||||
|
echo ERROR: Windows npm run build failed with exit code %NPM_EXIT%.
|
||||||
|
exit /b %NPM_EXIT%
|
||||||
|
)
|
||||||
|
set "WINDOWS_FRONTEND_BUILT=1"
|
||||||
|
|
||||||
|
:run_wsl
|
||||||
|
if defined WSLENV (
|
||||||
|
set "WSLENV=WINDOWS_FRONTEND_BUILT/u:%WSLENV%"
|
||||||
|
) else (
|
||||||
|
set "WSLENV=WINDOWS_FRONTEND_BUILT/u"
|
||||||
|
)
|
||||||
|
|
||||||
|
rem --cd keeps the working directory on the Windows checkout without fragile path munging.
|
||||||
|
wsl.exe -d %DEPLOY_WSL_DISTRO_EFFECTIVE% --cd "%ROOT%" -e bash "./deploy.sh" %*
|
||||||
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Canonical production deploy entrypoint.
|
||||||
|
# Windows users should prefer deploy.cmd, which switches into WSL and runs this script.
|
||||||
|
|
||||||
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
exec "$script_dir/scripts/deploy-production.sh" "$@"
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
|
; Install once on the app host:
|
||||||
|
; sudo cp deploy/supervisor/skinbase-ssr.conf /etc/supervisor/conf.d/skinbase-ssr.conf
|
||||||
|
; sudo supervisorctl reread && sudo supervisorctl update
|
||||||
|
; sudo supervisorctl start skinbase-ssr
|
||||||
|
;
|
||||||
|
; Path uses the stable app symlink (REMOTE_FOLDER), not a versioned release dir.
|
||||||
|
; Override node path if needed. Deploy restarts this via SSR_SUPERVISOR_PROGRAM=skinbase-ssr.
|
||||||
[program:skinbase-ssr]
|
[program:skinbase-ssr]
|
||||||
command=/usr/local/bin/node /opt/www/virtual/SkinbaseNova/bootstrap/ssr/ssr.js
|
command=/usr/bin/env node /opt/www/virtual/SkinbaseNova/bootstrap/ssr/ssr.js
|
||||||
|
directory=/opt/www/virtual/SkinbaseNova
|
||||||
process_name=%(program_name)s
|
process_name=%(program_name)s
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
user=www-data
|
user=skinbase
|
||||||
redirect_stderr=true
|
redirect_stderr=true
|
||||||
stdout_logfile=/var/log/skinbase_ssr.log
|
stdout_logfile=/var/log/skinbase_ssr.log
|
||||||
|
stdout_logfile_maxbytes=10MB
|
||||||
stopwaitsecs=10
|
stopwaitsecs=10
|
||||||
|
stopasgroup=true
|
||||||
|
killasgroup=true
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ Examples below are representative. For the full option list of any Artisan comma
|
|||||||
| Entry point | Why it is used | Example |
|
| Entry point | Why it is used | Example |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `php artisan` | Main Laravel CLI for all custom app commands listed below | `php artisan list --raw` |
|
| `php artisan` | Main Laravel CLI for all custom app commands listed below | `php artisan list --raw` |
|
||||||
| `bash sync.sh` | Main production deploy wrapper; delegates to the safe release-based production deploy script | `bash sync.sh` |
|
| `deploy.cmd` / `deploy.bat` | Windows Command Prompt production deploy entrypoint; switches into WSL and runs `deploy.sh` | `deploy.cmd` |
|
||||||
|
| `bash deploy.sh` | Canonical bash production deploy entrypoint; stages versioned releases and switches `current` | `bash deploy.sh` |
|
||||||
|
| `bash sync.sh` | Legacy alias for `bash deploy.sh` | `bash sync.sh` |
|
||||||
| `bash sync_dev.sh` | Push the development environment to the configured remote dev host | `bash sync_dev.sh` |
|
| `bash sync_dev.sh` | Push the development environment to the configured remote dev host | `bash sync_dev.sh` |
|
||||||
|
|
||||||
## Maintained Standalone Scripts
|
## Maintained Standalone Scripts
|
||||||
|
|||||||
+77
-39
@@ -1,18 +1,25 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
This repository uses a Bash-based production deploy flow.
|
This repository uses a Bash-based production deploy flow with a Windows Command Prompt wrapper.
|
||||||
|
|
||||||
## Normal deploy
|
## Normal deploy
|
||||||
|
|
||||||
Run the existing entrypoint:
|
Preferred entrypoints:
|
||||||
|
|
||||||
```bash
|
```bat
|
||||||
bash sync.sh
|
deploy.cmd
|
||||||
```
|
```
|
||||||
|
|
||||||
`bash sync.sh` delegates to the safe production deploy wrapper, which stages each deploy into a versioned release directory and switches traffic by updating the server-side `current` symlink.
|
```bash
|
||||||
|
bash deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
If you launch `bash sync.sh` from WSL against this Windows checkout, the script will automatically run the frontend build with `npm.cmd` on Windows so Rollup/Vite use the correct optional native package set.
|
`deploy.cmd` is the Windows Command Prompt entrypoint. It switches into WSL and runs `deploy.sh`.
|
||||||
|
`deploy.sh` is the canonical bash entrypoint and delegates to the safe production deploy wrapper, which stages each deploy into a versioned release directory and switches traffic by updating the server-side `current` symlink.
|
||||||
|
|
||||||
|
`bash sync.sh` remains as a legacy alias for the same flow.
|
||||||
|
|
||||||
|
`deploy.cmd` runs `npm.cmd run build` on Windows first, then enters WSL for rsync/ssh. That is required when the Ubuntu distro cannot execute Windows `.exe` files (`Exec format error` on `powershell.exe`). If WSL interop does work, `bash deploy.sh` can still launch `npm.cmd` through PowerShell. Local Linux `php`/`composer` are not required for a normal deploy; Artisan and Composer run on the production server. `--with-tests` uses WSL `php` when present, otherwise Windows `php.exe`.
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
|
|
||||||
@@ -28,7 +35,16 @@ This will:
|
|||||||
|
|
||||||
This is now the low-downtime default path for normal code and feature deploys.
|
This is now the low-downtime default path for normal code and feature deploys.
|
||||||
|
|
||||||
Each deploy generates a release ID automatically from UTC time and the local Git revision. Releases are retained under `REMOTE_RELEASE_ROOT/releases/<release-id>`, and production switches between them on the server by updating `REMOTE_RELEASE_ROOT/current`. The public/runtime path stays fixed at `REMOTE_FOLDER`, which is now treated as a stable symlink to the active release.
|
Each deploy generates:
|
||||||
|
|
||||||
|
- a monotonic local **build number** (stored in `var/deploy/build-number`)
|
||||||
|
- a **release ID** from UTC time + build number + Git revision, for example `20260829-141522-b42-a1b2c3d`
|
||||||
|
- local deploy history under `var/deploy/` (`latest.json`, `history.jsonl`, and per-run logs in `var/deploy/logs/`)
|
||||||
|
- remote metadata under `REMOTE_RELEASE_ROOT/deployments/<release-id>.json` and `current-release.json`
|
||||||
|
|
||||||
|
Console output includes step progress with elapsed time, rsync transfer progress, and a final duration summary. Override the build number with `--build-number N` or `BUILD_NUMBER=N` when needed. Dry-runs preview the next build number without consuming it.
|
||||||
|
|
||||||
|
Releases are retained under `REMOTE_RELEASE_ROOT/releases/<release-id>`, and production switches between them on the server by updating `REMOTE_RELEASE_ROOT/current`. The public/runtime path stays fixed at `REMOTE_FOLDER`, which is now treated as a stable symlink to the active release.
|
||||||
|
|
||||||
On the first deploy with this layout, the existing live folder is adopted into the release archive automatically and `REMOTE_FOLDER` is converted into that stable symlink path. After that, switching back to an older release does not require any local re-upload.
|
On the first deploy with this layout, the existing live folder is adopted into the release archive automatically and `REMOTE_FOLDER` is converted into that stable symlink path. After that, switching back to an older release does not require any local re-upload.
|
||||||
|
|
||||||
@@ -36,8 +52,12 @@ On the first deploy with this layout, the existing live folder is adopted into t
|
|||||||
|
|
||||||
Use a full upgrade when the release also needs broad Meilisearch work or non-code service operations.
|
Use a full upgrade when the release also needs broad Meilisearch work or non-code service operations.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
deploy.cmd --full-upgrade
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash sync.sh --full-upgrade
|
bash deploy.sh --full-upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
Full-upgrade mode:
|
Full-upgrade mode:
|
||||||
@@ -49,7 +69,7 @@ Full-upgrade mode:
|
|||||||
Example with service hooks:
|
Example with service hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash sync.sh --full-upgrade \
|
bash deploy.sh --full-upgrade \
|
||||||
--upgrade-pre-hook='sudo systemctl stop reverb' \
|
--upgrade-pre-hook='sudo systemctl stop reverb' \
|
||||||
--upgrade-post-hook='sudo systemctl restart reverb meilisearch'
|
--upgrade-post-hook='sudo systemctl restart reverb meilisearch'
|
||||||
```
|
```
|
||||||
@@ -59,39 +79,52 @@ You can also provide those hooks through environment variables instead of CLI fl
|
|||||||
```bash
|
```bash
|
||||||
FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' \
|
FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' \
|
||||||
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' \
|
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' \
|
||||||
bash sync.sh --full-upgrade
|
bash deploy.sh --full-upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy options
|
## Deploy options
|
||||||
|
|
||||||
|
```bat
|
||||||
|
deploy.cmd --skip-build
|
||||||
|
deploy.cmd --skip-migrate
|
||||||
|
deploy.cmd --no-maintenance
|
||||||
|
deploy.cmd --mode=full-upgrade
|
||||||
|
deploy.cmd --keep-releases=8
|
||||||
|
deploy.cmd --release-id=release-2026-04-25
|
||||||
|
deploy.cmd --build-number=100
|
||||||
|
deploy.cmd --no-rsync-progress
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash sync.sh --skip-build
|
bash deploy.sh --skip-build
|
||||||
bash sync.sh --skip-migrate
|
bash deploy.sh --skip-migrate
|
||||||
bash sync.sh --no-maintenance
|
bash deploy.sh --no-maintenance
|
||||||
bash sync.sh --mode=full-upgrade
|
bash deploy.sh --mode=full-upgrade
|
||||||
bash sync.sh --keep-releases=8
|
bash deploy.sh --keep-releases=8
|
||||||
bash sync.sh --release-id=release-2026-04-25
|
bash deploy.sh --release-id=release-2026-04-25
|
||||||
|
bash deploy.sh --build-number=100
|
||||||
|
bash deploy.sh --no-rsync-progress
|
||||||
```
|
```
|
||||||
|
|
||||||
Environment overrides:
|
Environment overrides:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
REMOTE_SERVER=user@example.com REMOTE_FOLDER=/var/www/app bash sync.sh
|
REMOTE_SERVER=user@example.com REMOTE_FOLDER=/var/www/app bash deploy.sh
|
||||||
REMOTE_RELEASE_ROOT=/var/www/app.releases RELEASE_RETENTION=8 bash sync.sh
|
REMOTE_RELEASE_ROOT=/var/www/app.releases RELEASE_RETENTION=8 bash deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also override the local build command explicitly:
|
You can also override the local build command explicitly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LOCAL_BUILD_COMMAND='npm run build' bash sync.sh
|
LOCAL_BUILD_COMMAND='npm run build' bash deploy.sh
|
||||||
LOCAL_BUILD_COMMAND='pnpm build' bash sync.sh
|
LOCAL_BUILD_COMMAND='pnpm build' bash deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Upgrade hooks can also be supplied via environment variables:
|
Upgrade hooks can also be supplied via environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' bash sync.sh --full-upgrade
|
FULL_UPGRADE_PRE_HOOK='sudo systemctl stop reverb' bash deploy.sh --full-upgrade
|
||||||
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' bash sync.sh --full-upgrade
|
FULL_UPGRADE_POST_HOOK='sudo systemctl restart reverb meilisearch' bash deploy.sh --full-upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rollback and release history
|
## Rollback and release history
|
||||||
@@ -126,26 +159,26 @@ Operational notes:
|
|||||||
- Each retained release already contains its own code and vendor tree, so rollback is primarily a symlink switch plus cache refresh and `queue:restart`.
|
- Each retained release already contains its own code and vendor tree, so rollback is primarily a symlink switch plus cache refresh and `queue:restart`.
|
||||||
- Rollback does not reverse database migrations. If a release includes incompatible schema changes, handle the database separately.
|
- Rollback does not reverse database migrations. If a release includes incompatible schema changes, handle the database separately.
|
||||||
- Release retention defaults to 5 releases and can be changed with `--keep-releases` or `RELEASE_RETENTION`.
|
- Release retention defaults to 5 releases and can be changed with `--keep-releases` or `RELEASE_RETENTION`.
|
||||||
- Release data lives outside the active app path by default at `REMOTE_FOLDER.releases`, so switching releases happens entirely on the production server.
|
- Reldeploy.sh --with-db-from=local
|
||||||
|
|
||||||
## Replace production database from local
|
|
||||||
|
|
||||||
This is intentionally separate from a normal deploy because it overwrites production data.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/push-db-to-prod.sh --force
|
|
||||||
```
|
|
||||||
|
|
||||||
Or combine it with deploy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash sync.sh --with-db-from=local
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When run interactively, the deploy script will ask you to confirm the exact remote server and type a confirmation phrase before replacing production data.
|
When run interactively, the deploy script will ask you to confirm the exact remote server and type a confirmation phrase before replacing production data.
|
||||||
|
|
||||||
For non-interactive use, pass both confirmations explicitly:
|
For non-interactive use, pass both confirmations explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy.sh --with-db-from=local \
|
||||||
|
--confirm-db-sync-target=klevze@server3.klevze.si \
|
||||||
|
--confirm-db-sync-phrase='replace production db from local'
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy compatibility still exists for:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deployinteractively, the deploy script will ask you to confirm the exact remote server and type a confirmation phrase before replacing production data.
|
||||||
|
|
||||||
|
For non-interactive use, pass both confirmations explicitly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash sync.sh --with-db-from=local \
|
bash sync.sh --with-db-from=local \
|
||||||
--confirm-db-sync-target=klevze@server3.klevze.si \
|
--confirm-db-sync-target=klevze@server3.klevze.si \
|
||||||
@@ -179,11 +212,16 @@ LOCAL_MYSQLDUMP_COMMAND='mysqldump --host=10.0.0.5 --port=3306 --user=app dbname
|
|||||||
|
|
||||||
## Safety notes
|
## Safety notes
|
||||||
|
|
||||||
- Normal deployments should use `bash sync.sh` without `--with-db`.
|
- Normal deployments should use `deploy.cmd` or `bash deploy.sh` without `--with-db`.
|
||||||
- Use `bash sync.sh --full-upgrade` only when the release also includes Meilisearch-wide refreshes or remote service changes.
|
- Use `--full-upgrade` only when the release also includes Meilisearch-wide refreshes or remote service changes.
|
||||||
- Use database replacement only for first-time bootstrap, staging, or an intentional full production reset.
|
- Use database replacement only for first-time bootstrap, staging, or an intentional full production reset.
|
||||||
- Use `bash scripts/rollback-production.sh --previous` for a fast server-side release switch when the last deploy needs to be reverted.
|
- Use `bash scripts/rollback-production.sh --previous` for a fast server-side release switch when the last deploy needs to be reverted.
|
||||||
- Route caching now runs through `php artisan optimize` in deploy automation; if that starts failing again, fix the route definitions instead of dropping route caching from deploy.
|
- Route caching now runs through `php artisan optimize` in deploy automation; if that starts failing again, fix the route definitions instead of dropping route caching from deploy.
|
||||||
|
- On Windows, prefer `deploy.cmd` so the process always enters WSL before rsync/ssh/php. It defaults to the local `Ubuntu` WSL distro. The production server itself runs Debian 13; that is separate from the local WSL choice. Override with `DEPLOY_WSL_DISTRO` only if needed. If WSL has no usable `~/.ssh` keys (common when the distro runs as root and keys live in `C:\Users\<you>\.ssh`), the script copies those Windows identities into a 0600 temp dir and uses them for BatchMode SSH. Override the source with `WINDOWS_SSH_DIR`.
|
||||||
|
- SSH still authenticates as `REMOTE_SERVER` (for example `klevze@host`), but remote rsync/composer/artisan/release work runs as `REMOTE_APP_USER` (default `skinbase`) via `sudo -n -u skinbase`. This keeps release files and runtime dirs owned by the app user and avoids prune blockers like `owner=skinbase mode=2700 .config`. Set `REMOTE_APP_USER=-` only to disable that and run as the SSH login user.
|
||||||
|
- Remote app-user shells start in `/tmp` (not the SSH user's home). The stable `REMOTE_FOLDER` symlink is left alone once it already points at `.../current`; only `current` is rewritten each deploy. That avoids `Permission denied` on root-owned parents like `/opt/www/virtual`.
|
||||||
|
- The deploy script refuses missing Vite/SSR build artifacts by default (`REQUIRE_BUILD_MANIFEST=1`), verifies SSH non-interactively first, and takes a local deploy lock so two overlapping deploys do not race.
|
||||||
|
- Inertia SSR restart is owned exclusively by Supervisor program `skinbase-ssr` (`deploy/supervisor/skinbase-ssr.conf`). The remote application phase runs as `skinbase`, while the restart is issued separately over the SSH deployment session as the privileged login account with `sudo -n /usr/bin/supervisorctl`. The deploy fails if privileged Supervisor access or the configured program is unavailable, and verifies that the program reaches `RUNNING`; it never starts or stops SSR through Artisan.
|
||||||
|
|
||||||
## Nginx upstream error pages
|
## Nginx upstream error pages
|
||||||
|
|
||||||
@@ -202,4 +240,4 @@ On the current `skinbase.org` vhost, the required FastCGI locations are:
|
|||||||
- `location ^~ /api/uploads/`
|
- `location ^~ /api/uploads/`
|
||||||
- `location = /index.php`
|
- `location = /index.php`
|
||||||
|
|
||||||
This intentionally intercepts only `502` and `504`, so Laravel remains responsible for normal `404`, `419`, `429`, `500`, and `503` rendering when the application is actually running.
|
This intentionally intercepts only `502` and `504`, so Laravel remains responsible for normal `404`, `419`, `429`, `500`, and `503` rendering when the application is actually running.
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared deploy observability helpers sourced by scripts/sync-safe-updated.sh.
|
||||||
|
# Keeps build numbers, local history, and progress logging out of the main script body.
|
||||||
|
|
||||||
|
: "${root_dir:?root_dir must be set before sourcing deploy-observability.sh}"
|
||||||
|
: "${local_folder:?local_folder must be set before sourcing deploy-observability.sh}"
|
||||||
|
|
||||||
|
build_number="${BUILD_NUMBER:-${build_number:-}}"
|
||||||
|
local_deploy_dir="${LOCAL_DEPLOY_DIR:-$root_dir/var/deploy}"
|
||||||
|
local_deploy_log_dir="${local_deploy_dir}/logs"
|
||||||
|
local_build_number_file="${local_deploy_dir}/build-number"
|
||||||
|
local_history_file="${local_deploy_dir}/history.jsonl"
|
||||||
|
local_latest_file="${local_deploy_dir}/latest.json"
|
||||||
|
local_deploy_log_file="${local_deploy_log_file:-}"
|
||||||
|
build_info_file="${build_info_file:-}"
|
||||||
|
git_sha="${git_sha:-unknown}"
|
||||||
|
git_branch="${git_branch:-unknown}"
|
||||||
|
git_dirty="${git_dirty:-0}"
|
||||||
|
deploy_started_epoch="${deploy_started_epoch:-$(date +%s)}"
|
||||||
|
deploy_started_utc="${deploy_started_utc:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
|
||||||
|
deploy_started_local="${deploy_started_local:-$(date +%Y-%m-%d\ %H:%M:%S\ %Z)}"
|
||||||
|
deploy_finished_epoch="${deploy_finished_epoch:-}"
|
||||||
|
deploy_status="${deploy_status:-running}"
|
||||||
|
deploy_phase_current="${deploy_phase_current:-0}"
|
||||||
|
phase_started_epoch="${phase_started_epoch:-$deploy_started_epoch}"
|
||||||
|
previous_release_meta="${previous_release_meta:-}"
|
||||||
|
rsync_show_progress="${rsync_show_progress:-1}"
|
||||||
|
declare -a deploy_phase_timings=("${deploy_phase_timings[@]+"${deploy_phase_timings[@]}"}")
|
||||||
|
|
||||||
|
format_duration() {
|
||||||
|
local total_seconds="${1:-0}"
|
||||||
|
local hours minutes seconds
|
||||||
|
|
||||||
|
if ! [[ "$total_seconds" =~ ^[0-9]+$ ]]; then
|
||||||
|
total_seconds=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
hours=$((total_seconds / 3600))
|
||||||
|
minutes=$(((total_seconds % 3600) / 60))
|
||||||
|
seconds=$((total_seconds % 60))
|
||||||
|
|
||||||
|
if (( hours > 0 )); then
|
||||||
|
printf '%dh %02dm %02ds' "$hours" "$minutes" "$seconds"
|
||||||
|
elif (( minutes > 0 )); then
|
||||||
|
printf '%dm %02ds' "$minutes" "$seconds"
|
||||||
|
else
|
||||||
|
printf '%ds' "$seconds"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed_since_start() {
|
||||||
|
local now
|
||||||
|
now="$(date +%s)"
|
||||||
|
format_duration "$((now - deploy_started_epoch))"
|
||||||
|
}
|
||||||
|
|
||||||
|
json_escape() {
|
||||||
|
local value="${1-}"
|
||||||
|
value="${value//\\/\\\\}"
|
||||||
|
value="${value//\"/\\\"}"
|
||||||
|
value="${value//$'\n'/\\n}"
|
||||||
|
value="${value//$'\r'/\\r}"
|
||||||
|
value="${value//$'\t'/\\t}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
append_deploy_log() {
|
||||||
|
local line="$1"
|
||||||
|
|
||||||
|
[[ -n "${local_deploy_log_file:-}" ]] || return 0
|
||||||
|
printf '%s\n' "$line" >> "$local_deploy_log_file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
obs_log_step() {
|
||||||
|
local message="$1"
|
||||||
|
local now
|
||||||
|
local line
|
||||||
|
|
||||||
|
now="$(date +%s)"
|
||||||
|
deploy_phase_current=$((deploy_phase_current + 1))
|
||||||
|
phase_started_epoch="$now"
|
||||||
|
line="$(printf '[step %s] (+%s) %s' "$deploy_phase_current" "$(elapsed_since_start)" "$message")"
|
||||||
|
|
||||||
|
printf '\n%s\n' "$line"
|
||||||
|
append_deploy_log "$(date -u +%Y-%m-%dT%H:%M:%SZ) STEP ${line}"
|
||||||
|
}
|
||||||
|
|
||||||
|
obs_log_info() {
|
||||||
|
local message="$1"
|
||||||
|
printf ' -> %s\n' "$message"
|
||||||
|
append_deploy_log "$(date -u +%Y-%m-%dT%H:%M:%SZ) INFO ${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
obs_log_warn() {
|
||||||
|
local message="$1"
|
||||||
|
printf 'WARN: %s\n' "$message" >&2
|
||||||
|
append_deploy_log "$(date -u +%Y-%m-%dT%H:%M:%SZ) WARN ${message}"
|
||||||
|
}
|
||||||
|
|
||||||
|
obs_die() {
|
||||||
|
printf 'ERROR: %s\n' "$1" >&2
|
||||||
|
append_deploy_log "$(date -u +%Y-%m-%dT%H:%M:%SZ) ERROR $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_phase_complete() {
|
||||||
|
local label="${1:-phase}"
|
||||||
|
local now
|
||||||
|
local elapsed
|
||||||
|
|
||||||
|
now="$(date +%s)"
|
||||||
|
elapsed=$((now - phase_started_epoch))
|
||||||
|
deploy_phase_timings+=("${label}=$(format_duration "$elapsed")")
|
||||||
|
obs_log_info "Phase finished in $(format_duration "$elapsed") (total $(elapsed_since_start))"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_local_deploy_dirs() {
|
||||||
|
mkdir -p "$local_deploy_dir" "$local_deploy_log_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Full-tree dirty checks (git diff / status / describe --dirty) can take minutes
|
||||||
|
# on WSL /mnt/<drive> checkouts because every file is stat'd over 9p. Keep deploy
|
||||||
|
# metadata fast: short SHA/branch always; unstaged dirty scans are skipped on
|
||||||
|
# slow filesystems and otherwise hard-timeout'd.
|
||||||
|
git_metadata_timeout_seconds="${GIT_METADATA_TIMEOUT_SECONDS:-8}"
|
||||||
|
|
||||||
|
git_workdir_is_slow() {
|
||||||
|
case "$local_folder" in
|
||||||
|
/mnt/[a-zA-Z]/*|/mnt/[a-zA-Z])
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local fstype=""
|
||||||
|
if command -v findmnt >/dev/null 2>&1; then
|
||||||
|
fstype="$(findmnt -n -o FSTYPE --target "$local_folder" 2>/dev/null || true)"
|
||||||
|
case "$fstype" in
|
||||||
|
9p|drvfs|cifs|nfs|nfs4|fuse|fuseblk|fuse.*)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_git_with_timeout() {
|
||||||
|
local timeout_seconds="${1:-$git_metadata_timeout_seconds}"
|
||||||
|
shift
|
||||||
|
|
||||||
|
# Do not use timeout --foreground: that leaves git children unkilled.
|
||||||
|
# -k 2 sends SIGKILL if SIGTERM cannot interrupt a stuck 9p stat.
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout -k 2 "$timeout_seconds" git -C "$local_folder" "$@"
|
||||||
|
return $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$local_folder" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns: 0 dirty, 1 clean, 2 timeout/unknown, 3 skipped unstaged scan (slow FS).
|
||||||
|
detect_git_dirty_fast() {
|
||||||
|
local status_output=""
|
||||||
|
local rc=0
|
||||||
|
|
||||||
|
# Staged changes are cheap (index vs HEAD, no worktree walk).
|
||||||
|
run_git_with_timeout 3 diff --cached --quiet >/dev/null 2>&1 || rc=$?
|
||||||
|
if [[ "$rc" -eq 1 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
if [[ "$rc" -ne 0 ]]; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if git_workdir_is_slow; then
|
||||||
|
return 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Porcelain still stats the worktree; only run it on native filesystems.
|
||||||
|
rc=0
|
||||||
|
status_output="$(run_git_with_timeout "$git_metadata_timeout_seconds" -c core.untrackedCache=false status --porcelain=v1 -uno 2>/dev/null)" || rc=$?
|
||||||
|
if [[ "$rc" -eq 0 ]]; then
|
||||||
|
if [[ -n "${status_output//[[:space:]]/}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_git_metadata() {
|
||||||
|
local dirty_rc=1
|
||||||
|
|
||||||
|
git_sha="unknown"
|
||||||
|
git_branch="unknown"
|
||||||
|
git_dirty=0
|
||||||
|
|
||||||
|
if ! command -v git >/dev/null 2>&1 || ! git -C "$local_folder" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf ' -> Collecting local Git metadata (SHA/branch; dirty check is bounded)...\n'
|
||||||
|
|
||||||
|
git_sha="$(git -C "$local_folder" rev-parse --short HEAD 2>/dev/null || printf 'unknown')"
|
||||||
|
git_branch="$(git -C "$local_folder" branch --show-current 2>/dev/null || printf 'DETACHED')"
|
||||||
|
|
||||||
|
# Capture without tripping set -e (clean/timeout/skip are non-zero).
|
||||||
|
dirty_rc=1
|
||||||
|
detect_git_dirty_fast && dirty_rc=0 || dirty_rc=$?
|
||||||
|
if [[ "$dirty_rc" -eq 0 ]]; then
|
||||||
|
git_dirty=1
|
||||||
|
elif [[ "$dirty_rc" -eq 3 ]]; then
|
||||||
|
git_dirty=0
|
||||||
|
printf ' -> Skipping unstaged Git dirty scan on this filesystem (WSL /mnt or network mount); using staged-only check.\n' >&2
|
||||||
|
elif [[ "$dirty_rc" -eq 2 ]]; then
|
||||||
|
git_dirty=0
|
||||||
|
printf ' -> Git dirty check timed out after %ss on this filesystem; continuing without dirty marker.\n' \
|
||||||
|
"$git_metadata_timeout_seconds" >&2
|
||||||
|
else
|
||||||
|
git_dirty=0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
peek_next_build_number() {
|
||||||
|
local existing=""
|
||||||
|
local next=1
|
||||||
|
|
||||||
|
if [[ -f "$local_build_number_file" ]]; then
|
||||||
|
existing="$(tr -d '[:space:]' < "$local_build_number_file" 2>/dev/null || true)"
|
||||||
|
if [[ "$existing" =~ ^[0-9]+$ ]]; then
|
||||||
|
next=$((existing + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$next"
|
||||||
|
}
|
||||||
|
|
||||||
|
allocate_build_number() {
|
||||||
|
local existing=""
|
||||||
|
local persist=1
|
||||||
|
|
||||||
|
ensure_local_deploy_dirs
|
||||||
|
|
||||||
|
# Dry-runs never consume the counter unless an explicit override is provided for labeling only.
|
||||||
|
if [[ "${dry_run:-0}" -eq 1 ]]; then
|
||||||
|
persist=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$build_number" ]]; then
|
||||||
|
[[ "$build_number" =~ ^[1-9][0-9]*$ ]] || obs_die "Build number must be a positive integer. Received: $build_number"
|
||||||
|
else
|
||||||
|
build_number="$(peek_next_build_number)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$build_number" =~ ^[1-9][0-9]*$ ]] || obs_die "Build number must be a positive integer. Received: $build_number"
|
||||||
|
|
||||||
|
if [[ "$persist" -eq 1 ]]; then
|
||||||
|
if [[ -f "$local_build_number_file" ]]; then
|
||||||
|
existing="$(tr -d '[:space:]' < "$local_build_number_file" 2>/dev/null || true)"
|
||||||
|
if [[ "$existing" =~ ^[0-9]+$ ]] && (( build_number < existing )); then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
printf '%s\n' "$build_number" > "$local_build_number_file"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' "$build_number" > "$local_build_number_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_local_deploy_log_file() {
|
||||||
|
local stamp
|
||||||
|
|
||||||
|
ensure_local_deploy_dirs
|
||||||
|
stamp="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
|
local_deploy_log_file="${local_deploy_log_dir}/deploy-${stamp}-b${build_number}.log"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'Skinbase production deploy log\n'
|
||||||
|
printf 'started_at_utc=%s\n' "$deploy_started_utc"
|
||||||
|
printf 'started_at_local=%s\n' "$deploy_started_local"
|
||||||
|
printf 'build_number=%s\n' "$build_number"
|
||||||
|
printf 'release_id=%s\n' "${release_id:-pending}"
|
||||||
|
printf 'mode=%s\n' "${deploy_mode:-normal}"
|
||||||
|
printf 'source=%s\n' "$local_folder"
|
||||||
|
printf 'target=%s:%s\n' "${remote_server:-}" "${remote_folder:-}"
|
||||||
|
printf 'git_sha=%s\n' "$git_sha"
|
||||||
|
printf 'git_branch=%s\n' "$git_branch"
|
||||||
|
printf 'git_dirty=%s\n' "$git_dirty"
|
||||||
|
printf '%s\n' '---'
|
||||||
|
} > "$local_deploy_log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_build_info_file() {
|
||||||
|
build_info_file="${local_folder}/build-info.json"
|
||||||
|
|
||||||
|
cat > "$build_info_file" <<EOF
|
||||||
|
{
|
||||||
|
"build_number": ${build_number},
|
||||||
|
"release_id": "$(json_escape "${release_id:-}")",
|
||||||
|
"deployed_at_utc": "$(json_escape "$deploy_started_utc")",
|
||||||
|
"git_sha": "$(json_escape "$git_sha")",
|
||||||
|
"git_branch": "$(json_escape "$git_branch")",
|
||||||
|
"git_dirty": ${git_dirty},
|
||||||
|
"deployment_mode": "$(json_escape "${deploy_mode:-normal}")",
|
||||||
|
"remote_server": "$(json_escape "${remote_server:-}")",
|
||||||
|
"remote_folder": "$(json_escape "${remote_folder:-}")"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
obs_log_info "Wrote build metadata: $build_info_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_local_deploy_history() {
|
||||||
|
local status="${1:-unknown}"
|
||||||
|
local previous_release="${2-}"
|
||||||
|
local finished_utc finished_local duration_seconds duration_human
|
||||||
|
local timings_json="[]"
|
||||||
|
local timing
|
||||||
|
local first=1
|
||||||
|
|
||||||
|
deploy_finished_epoch="$(date +%s)"
|
||||||
|
finished_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
finished_local="$(date +%Y-%m-%d\ %H:%M:%S\ %Z)"
|
||||||
|
duration_seconds=$((deploy_finished_epoch - deploy_started_epoch))
|
||||||
|
duration_human="$(format_duration "$duration_seconds")"
|
||||||
|
deploy_status="$status"
|
||||||
|
|
||||||
|
ensure_local_deploy_dirs
|
||||||
|
|
||||||
|
if (( ${#deploy_phase_timings[@]} > 0 )); then
|
||||||
|
timings_json="["
|
||||||
|
first=1
|
||||||
|
for timing in "${deploy_phase_timings[@]}"; do
|
||||||
|
if (( first )); then
|
||||||
|
first=0
|
||||||
|
else
|
||||||
|
timings_json+=","
|
||||||
|
fi
|
||||||
|
timings_json+="\"$(json_escape "$timing")\""
|
||||||
|
done
|
||||||
|
timings_json+="]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$local_latest_file" <<EOF
|
||||||
|
{
|
||||||
|
"status": "$(json_escape "$status")",
|
||||||
|
"build_number": ${build_number:-0},
|
||||||
|
"release_id": "$(json_escape "${release_id:-}")",
|
||||||
|
"previous_release_id": "$(json_escape "$previous_release")",
|
||||||
|
"started_at_utc": "$(json_escape "$deploy_started_utc")",
|
||||||
|
"started_at_local": "$(json_escape "$deploy_started_local")",
|
||||||
|
"finished_at_utc": "$(json_escape "$finished_utc")",
|
||||||
|
"finished_at_local": "$(json_escape "$finished_local")",
|
||||||
|
"duration_seconds": ${duration_seconds},
|
||||||
|
"duration_human": "$(json_escape "$duration_human")",
|
||||||
|
"deployment_mode": "$(json_escape "${deploy_mode:-normal}")",
|
||||||
|
"dry_run": ${dry_run:-0},
|
||||||
|
"git_sha": "$(json_escape "$git_sha")",
|
||||||
|
"git_branch": "$(json_escape "$git_branch")",
|
||||||
|
"git_dirty": ${git_dirty},
|
||||||
|
"remote_server": "$(json_escape "${remote_server:-}")",
|
||||||
|
"remote_folder": "$(json_escape "${remote_folder:-}")",
|
||||||
|
"log_file": "$(json_escape "${local_deploy_log_file:-}")",
|
||||||
|
"phase_timings": ${timings_json}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat "$local_latest_file" >> "$local_history_file"
|
||||||
|
printf '\n' >> "$local_history_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_deploy_banner() {
|
||||||
|
local dirty_marker=""
|
||||||
|
local dry_marker=""
|
||||||
|
|
||||||
|
if [[ "${git_dirty:-0}" -eq 1 ]]; then
|
||||||
|
dirty_marker=" [dirty]"
|
||||||
|
fi
|
||||||
|
if [[ "${dry_run:-0}" -eq 1 ]]; then
|
||||||
|
dry_marker=" (dry-run preview)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n'
|
||||||
|
printf '============================================================\n'
|
||||||
|
printf ' Skinbase production deploy\n'
|
||||||
|
printf '============================================================\n'
|
||||||
|
printf ' Build number : %s%s\n' "${build_number:-pending}" "$dry_marker"
|
||||||
|
printf ' Release : %s\n' "${release_id:-pending}"
|
||||||
|
printf ' Mode : %s\n' "${deploy_mode:-normal}"
|
||||||
|
printf ' Started : %s\n' "$deploy_started_local"
|
||||||
|
printf ' Source : %s\n' "$local_folder"
|
||||||
|
printf ' Target : %s:%s\n' "${remote_server:-}" "${remote_folder:-}"
|
||||||
|
printf ' Git : %s (%s)%s\n' "$git_sha" "$git_branch" "$dirty_marker"
|
||||||
|
printf ' Local log : %s\n' "${local_deploy_log_file:-n/a}"
|
||||||
|
printf '============================================================\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize_local_deploy_observability() {
|
||||||
|
local exit_code="${1:-0}"
|
||||||
|
local final_status="success"
|
||||||
|
|
||||||
|
if [[ "$exit_code" -ne 0 ]]; then
|
||||||
|
final_status="failed"
|
||||||
|
elif [[ "${dry_run:-0}" -eq 1 ]]; then
|
||||||
|
final_status="dry-run"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${build_number:-}" && "$deploy_status" == "running" ]]; then
|
||||||
|
write_local_deploy_history "$final_status" "${previous_release_meta:-}"
|
||||||
|
if [[ -n "${local_deploy_log_file:-}" ]]; then
|
||||||
|
{
|
||||||
|
printf '%s\n' '---'
|
||||||
|
printf 'status=%s\n' "$final_status"
|
||||||
|
printf 'exit_code=%s\n' "$exit_code"
|
||||||
|
printf 'finished_at_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
printf 'duration=%s\n' "$(elapsed_since_start)"
|
||||||
|
printf 'release_id=%s\n' "${release_id:-}"
|
||||||
|
printf 'build_number=%s\n' "${build_number:-}"
|
||||||
|
} >> "$local_deploy_log_file" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
release_id_with_build() {
|
||||||
|
local timestamp
|
||||||
|
local vcs_fragment="manual"
|
||||||
|
|
||||||
|
if [[ -n "${release_id:-}" ]]; then
|
||||||
|
printf '%s' "$release_id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
timestamp="$(date -u +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
|
# Never use `git describe --dirty` here: --dirty walks the whole work tree and
|
||||||
|
# can hang for minutes on WSL /mnt/* checkouts. Prefer already-collected SHA.
|
||||||
|
if [[ -n "${git_sha:-}" && "$git_sha" != "unknown" ]]; then
|
||||||
|
vcs_fragment="$git_sha"
|
||||||
|
if [[ "${git_dirty:-0}" -eq 1 ]]; then
|
||||||
|
vcs_fragment="${vcs_fragment}-dirty"
|
||||||
|
fi
|
||||||
|
elif command -v git >/dev/null 2>&1 && git -C "$local_folder" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
vcs_fragment="$(git -C "$local_folder" rev-parse --short HEAD 2>/dev/null || printf 'manual')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
vcs_fragment="$(sanitize_release_fragment "$vcs_fragment")"
|
||||||
|
[[ -n "$vcs_fragment" ]] || vcs_fragment="manual"
|
||||||
|
|
||||||
|
if [[ -n "${build_number:-}" ]]; then
|
||||||
|
printf '%s-b%s-%s' "$timestamp" "$build_number" "$vcs_fragment"
|
||||||
|
else
|
||||||
|
printf '%s-%s' "$timestamp" "$vcs_fragment"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Production deploy implementation entry used by deploy.sh / deploy.cmd.
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
exec "$script_dir/sync-safe-updated.sh" "$@"
|
exec "$script_dir/sync-safe-updated.sh" "$@"
|
||||||
|
|||||||
@@ -316,7 +316,20 @@ fi
|
|||||||
|
|
||||||
log_step "Switching current release to ${target_release}"
|
log_step "Switching current release to ${target_release}"
|
||||||
ln -sfn "$target_release_path" "$current_link"
|
ln -sfn "$target_release_path" "$current_link"
|
||||||
ln -sfn "$current_link" "$REMOTE_FOLDER"
|
# The public app path is a klevze-owned symlink to .../releases/current. Rewriting
|
||||||
|
# current is enough; ln on REMOTE_FOLDER fails with Permission denied for skinbase.
|
||||||
|
if [[ -L "$REMOTE_FOLDER" ]]; then
|
||||||
|
literal="$(readlink -n "$REMOTE_FOLDER" || true)"
|
||||||
|
if [[ "$literal" != "$current_link" ]]; then
|
||||||
|
resolved="$(readlink -f "$REMOTE_FOLDER" 2>/dev/null || true)"
|
||||||
|
expected="$(readlink -f "$current_link" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$resolved" || -z "$expected" || "$resolved" != "$expected" ]]; then
|
||||||
|
ln -sfn "$current_link" "$REMOTE_FOLDER" || die "Cannot retarget ${REMOTE_FOLDER} to ${current_link} (parent directory not writable by $(id -un)). The current symlink was updated; fix the public app symlink as the SSH login user if needed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ ! -e "$REMOTE_FOLDER" ]]; then
|
||||||
|
ln -sfn "$current_link" "$REMOTE_FOLDER" || die "Cannot create ${REMOTE_FOLDER} -> ${current_link}. Create that symlink as the SSH login user."
|
||||||
|
fi
|
||||||
|
|
||||||
cd "$REMOTE_FOLDER"
|
cd "$REMOTE_FOLDER"
|
||||||
|
|
||||||
|
|||||||
+903
-94
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Legacy alias. Prefer: deploy.cmd (Windows) or bash deploy.sh (WSL/Linux).
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
exec "$script_dir/scripts/deploy-production.sh" "$@"
|
exec "$script_dir/deploy.sh" "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user