Fix auto-update pipeline bugs discovered in e2e testing

- Fix container-to-host path mapping in trigger.json (stagingDir
  was using container path /app/updates/ instead of host path
  /opt/dashcaddy/updates/)
- Fix download race condition: primary download's async unlink
  could delete mirror download's file — use unlinkSync before retry
- Fix DASHCADDY_COMMIT build arg not passed to docker compose build
  (was set as env var, now uses --build-arg)
- Remove MakeDirectory=yes from systemd path unit (was creating
  trigger.json as directory instead of file)
- Remove unused 'tar' npm module import
- Add mirror fallback for tarball downloads (not just version checks)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 03:32:08 -08:00
parent ffa6966fd3
commit 2d1944fd55
4 changed files with 25 additions and 9 deletions

View File

@@ -5,7 +5,7 @@
set -euo pipefail set -euo pipefail
readonly REPO_URL="http://100.98.123.59:3000/sami7777/dashcaddy.git" readonly REPO_URL="http://sami7777:2728bb667201841b08cb35ac101ffe52838f7d11@100.98.123.59:3000/sami7777/dashcaddy.git"
readonly RELEASE_DIR="/var/www/get.dashcaddy.net/release" readonly RELEASE_DIR="/var/www/get.dashcaddy.net/release"
readonly BUILD_DIR="/tmp/dashcaddy-build-$$" readonly BUILD_DIR="/tmp/dashcaddy-build-$$"
readonly MIRROR_HOST="root@100.98.123.59" # Contabo DE readonly MIRROR_HOST="root@100.98.123.59" # Contabo DE

View File

@@ -162,7 +162,7 @@ main() {
# Step 3: Rebuild container # Step 3: Rebuild container
log "Step 3: Building new container image" log "Step 3: Building new container image"
cd "$compose_dir" cd "$compose_dir"
if ! DASHCADDY_COMMIT="$commit" docker compose build --quiet 2>&1; then if ! docker compose build --build-arg DASHCADDY_COMMIT="$commit" --quiet 2>&1; then
log "ERROR: docker compose build failed, rolling back" log "ERROR: docker compose build failed, rolling back"
restore_backup "$api_dir" "$from_version" restore_backup "$api_dir" "$from_version"
local elapsed=$(( $(date +%s) - start_time )) local elapsed=$(( $(date +%s) - start_time ))

View File

@@ -4,7 +4,6 @@ Documentation=https://dashcaddy.net
[Path] [Path]
PathChanged=/opt/dashcaddy/updates/trigger.json PathChanged=/opt/dashcaddy/updates/trigger.json
MakeDirectory=yes
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -42,6 +42,9 @@ class SelfUpdater extends EventEmitter {
updateUrl: options.updateUrl || DEFAULTS.UPDATE_URL, updateUrl: options.updateUrl || DEFAULTS.UPDATE_URL,
mirrorUrl: options.mirrorUrl || DEFAULTS.MIRROR_URL, mirrorUrl: options.mirrorUrl || DEFAULTS.MIRROR_URL,
updatesDir: options.updatesDir || DEFAULTS.UPDATES_DIR, updatesDir: options.updatesDir || DEFAULTS.UPDATES_DIR,
// hostUpdatesDir is the HOST path that maps to updatesDir inside the container.
// Used when writing trigger.json so the host-side script can find staging files.
hostUpdatesDir: options.hostUpdatesDir || (isWindows ? options.updatesDir || DEFAULTS.UPDATES_DIR : '/opt/dashcaddy/updates'),
apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR, apiSourceDir: options.apiSourceDir || DEFAULTS.API_SOURCE_DIR,
frontendDir: options.frontendDir || DEFAULTS.FRONTEND_DIR, frontendDir: options.frontendDir || DEFAULTS.FRONTEND_DIR,
maxBackups: parseInt(options.maxBackups || DEFAULTS.MAX_BACKUPS, 10), maxBackups: parseInt(options.maxBackups || DEFAULTS.MAX_BACKUPS, 10),
@@ -102,12 +105,14 @@ class SelfUpdater extends EventEmitter {
this.status = 'checking'; this.status = 'checking';
try { try {
let remote; let remote;
let sourceUrl = this.config.updateUrl;
try { try {
remote = await this._fetchJson(`${this.config.updateUrl}/version.json`); remote = await this._fetchJson(`${this.config.updateUrl}/version.json`);
} catch (primaryErr) { } catch (primaryErr) {
console.warn('[SelfUpdater] Primary server failed:', primaryErr.message, '— trying mirror'); console.warn('[SelfUpdater] Primary server failed:', primaryErr.message, '— trying mirror');
try { try {
remote = await this._fetchJson(`${this.config.mirrorUrl}/version.json`); remote = await this._fetchJson(`${this.config.mirrorUrl}/version.json`);
sourceUrl = this.config.mirrorUrl;
} catch (mirrorErr) { } catch (mirrorErr) {
this.status = 'idle'; this.status = 'idle';
this.lastCheckTime = Date.now(); this.lastCheckTime = Date.now();
@@ -120,7 +125,7 @@ class SelfUpdater extends EventEmitter {
const available = this._isNewer(local, remote); const available = this._isNewer(local, remote);
this.lastCheckTime = Date.now(); this.lastCheckTime = Date.now();
this.lastCheckResult = { available, local, remote }; this.lastCheckResult = { available, local, remote, sourceUrl };
this.status = 'idle'; this.status = 'idle';
if (available) { if (available) {
@@ -147,13 +152,21 @@ class SelfUpdater extends EventEmitter {
const stagingDir = path.join(this.config.updatesDir, 'staging'); const stagingDir = path.join(this.config.updatesDir, 'staging');
try { try {
// 1. Download // 1. Download (try primary, fallback to mirror)
this.status = 'downloading'; this.status = 'downloading';
this.emit('update-progress', { step: 'downloading', version: remoteInfo.version }); this.emit('update-progress', { step: 'downloading', version: remoteInfo.version });
const tarballUrl = `${this.config.updateUrl}/${remoteInfo.tarball}`;
const tarballPath = path.join(this.config.updatesDir, remoteInfo.tarball); const tarballPath = path.join(this.config.updatesDir, remoteInfo.tarball);
await this._downloadFile(tarballUrl, tarballPath); const primaryUrl = `${this.config.updateUrl}/${remoteInfo.tarball}`;
const mirrorUrl = `${this.config.mirrorUrl}/${remoteInfo.tarball}`;
try {
await this._downloadFile(primaryUrl, tarballPath);
} catch (dlErr) {
console.warn('[SelfUpdater] Primary download failed:', dlErr.message, '— trying mirror');
// Ensure file is fully cleaned up before mirror attempt
try { fs.unlinkSync(tarballPath); } catch (_) {}
await this._downloadFile(mirrorUrl, tarballPath);
}
// 2. Verify SHA-256 // 2. Verify SHA-256
const hash = await this._computeSha256(tarballPath); const hash = await this._computeSha256(tarballPath);
@@ -184,12 +197,14 @@ class SelfUpdater extends EventEmitter {
this.status = 'waiting'; this.status = 'waiting';
this.emit('update-progress', { step: 'triggering-rebuild', version: remoteInfo.version }); this.emit('update-progress', { step: 'triggering-rebuild', version: remoteInfo.version });
// Convert container path to host path for trigger.json
const hostApiSrc = apiSrc.replace(this.config.updatesDir, this.config.hostUpdatesDir);
const trigger = { const trigger = {
action: 'update', action: 'update',
version: remoteInfo.version, version: remoteInfo.version,
commit: remoteInfo.commit, commit: remoteInfo.commit,
fromVersion: local.version, fromVersion: local.version,
stagingDir: apiSrc, stagingDir: hostApiSrc,
apiSourceDir: this.config.apiSourceDir, apiSourceDir: this.config.apiSourceDir,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };
@@ -285,11 +300,12 @@ class SelfUpdater extends EventEmitter {
} }
const local = this.getLocalVersion(); const local = this.getLocalVersion();
const hostBackupDir = backupDir.replace(this.config.updatesDir, this.config.hostUpdatesDir);
const trigger = { const trigger = {
action: 'rollback', action: 'rollback',
version: version, version: version,
fromVersion: local.version, fromVersion: local.version,
stagingDir: backupDir, stagingDir: hostBackupDir,
apiSourceDir: this.config.apiSourceDir, apiSourceDir: this.config.apiSourceDir,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };
@@ -508,6 +524,7 @@ const selfUpdater = new SelfUpdater({
updateUrl: process.env.DASHCADDY_UPDATE_URL, updateUrl: process.env.DASHCADDY_UPDATE_URL,
mirrorUrl: process.env.DASHCADDY_MIRROR_URL, mirrorUrl: process.env.DASHCADDY_MIRROR_URL,
updatesDir: process.env.DASHCADDY_UPDATES_DIR, updatesDir: process.env.DASHCADDY_UPDATES_DIR,
hostUpdatesDir: process.env.DASHCADDY_HOST_UPDATES_DIR,
apiSourceDir: process.env.DASHCADDY_API_SOURCE_DIR, apiSourceDir: process.env.DASHCADDY_API_SOURCE_DIR,
frontendDir: process.env.DASHCADDY_FRONTEND_DIR, frontendDir: process.env.DASHCADDY_FRONTEND_DIR,
}); });