feat: add in-app self-updater (App Settings → Updates)

Update Codeman from the web UI: a "Check for updates" button queries GitHub
for the latest tagged release (git ls-remote fallback) and shows release
notes; "Update now" runs git checkout <tag> → npm install → npm run build →
restart, streaming live progress that survives the service restart.

- Release-tag channel; dirty trees auto-stashed (left for manual git stash pop)
- Cross-platform restart: systemd / launchd / manual, detected at runtime
- Updater runs detached (systemd-run --scope on Linux, setsid on macOS) so the
  restart it triggers can't kill the build mid-flight
- Build-failure rollback to the pre-update commit; boot reconcile with an
  update-id/freshness guard; 409 concurrency lock; runner staged outside the
  repo; strict tag validation; CODEMAN_DISABLE_SELF_UPDATE kill-switch
- Endpoints: GET /api/system/update/check, POST /api/system/update,
  GET /api/system/update/status

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arkon
2026-06-09 01:55:54 +02:00
co-authored by Claude Opus 4.8
parent 3503b6ae55
commit 543be8a85b
9 changed files with 1222 additions and 0 deletions
+18
View File
@@ -1078,6 +1078,24 @@
<span id="tunnelUploadUrlDisplay" class="settings-item-value" style="cursor:pointer; text-decoration:underline; font-family:monospace; font-size:12px" title="Click to copy"></span>
</div>
<!-- Updates Section -->
<div class="settings-section-header">Updates</div>
<div class="settings-item" title="Codeman version currently running">
<span class="settings-item-label">Current Version</span>
<span class="settings-item-value" id="updateCurrentVersion" style="font-family:monospace">&mdash;</span>
</div>
<div class="settings-item" id="updateCheckRow" title="Check GitHub for a newer Codeman release">
<span class="settings-item-label">Check for Updates</span>
<button class="btn-toolbar btn-sm" id="updateCheckBtn" onclick="app.checkForUpdate()">Check now</button>
</div>
<div id="updateResult" style="display:none; padding:4px 2px 8px; font-size:13px; color:var(--text-secondary)"></div>
<div class="settings-item" id="updateActionRow" style="display:none">
<span class="settings-item-label" id="updateActionLabel">Update available</span>
<button class="btn-toolbar btn-sm btn-primary" id="updateNowBtn" onclick="app.startSelfUpdate()">Update now</button>
</div>
<div id="updateNotes" style="display:none; max-height:160px; overflow:auto; padding:8px 10px; margin:4px 0 8px; font-size:12px; line-height:1.45; white-space:pre-wrap; word-break:break-word; background:rgba(127,127,127,0.08); border:1px solid var(--border); border-radius:6px"></div>
<div id="updateProgress" style="display:none; padding:8px 10px; margin:4px 0 8px; font-size:13px; border:1px solid var(--border); border-radius:6px"></div>
</div>
</div>
<!-- Tab-Switch Tab -->