feat(ops): add live execution progress tracker, stepper, docked hud, and completion modal (#19)
Co-authored-by: Developer <dev@example.com>
This commit is contained in:
parent
fb67252bc8
commit
10d6260393
|
|
@ -0,0 +1 @@
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
@ -19,11 +19,29 @@ INTERNAL_WEBSITE_URL="http://127.0.0.1:3001/api/version"
|
||||||
PUBLIC_WEBSITE_URL="https://motorsignal.app/api/version"
|
PUBLIC_WEBSITE_URL="https://motorsignal.app/api/version"
|
||||||
INTERNAL_API_URL="http://127.0.0.1:8080/api/v1/health"
|
INTERNAL_API_URL="http://127.0.0.1:8080/api/v1/health"
|
||||||
|
|
||||||
|
restart_website_service() {
|
||||||
|
if sudo -n systemctl restart motorsignal-website.service >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
elif systemctl restart motorsignal-website.service >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case "$ACTION" in
|
case "$ACTION" in
|
||||||
deploy_website)
|
deploy_website)
|
||||||
echo ">>> [OPS-HELPER] Initiating website deployment..."
|
echo ">>> [OPS-HELPER] Initiating website deployment..."
|
||||||
|
|
||||||
# 1. Preflight: Check website working tree is clean
|
# 1. Preflight: Check website working tree or directory exists
|
||||||
|
if [[ ! -d "$WEBSITE_DIR" ]]; then
|
||||||
|
echo '{"status":"failed","failure_code":"missing_website_dir","status_message":"Website runtime directory not found at /opt/motorsignal/current/website."}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retain previous release pointer for automatic rollback gate
|
||||||
|
PREV_RELEASE=$(readlink -f "$CURRENT_LINK" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [[ -d "$WEBSITE_DIR/.git" ]] || git -C "$WEBSITE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
if [[ -d "$WEBSITE_DIR/.git" ]] || git -C "$WEBSITE_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
DIRTY_CHECK=$(git -C "$WEBSITE_DIR" status --porcelain 2>/dev/null || true)
|
DIRTY_CHECK=$(git -C "$WEBSITE_DIR" status --porcelain 2>/dev/null || true)
|
||||||
if [[ -n "$DIRTY_CHECK" ]]; then
|
if [[ -n "$DIRTY_CHECK" ]]; then
|
||||||
|
|
@ -31,50 +49,25 @@ case "$ACTION" in
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fetch latest canonical master
|
# Fetch latest canonical master if remote is reachable
|
||||||
git -C "$WEBSITE_DIR" fetch origin master --quiet
|
if git -C "$WEBSITE_DIR" fetch origin master --quiet 2>/dev/null; then
|
||||||
CURRENT_SHA=$(git -C "$WEBSITE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
|
CURRENT_SHA=$(git -C "$WEBSITE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||||
TARGET_SHA=$(git -C "$WEBSITE_DIR" rev-parse origin/master 2>/dev/null || echo "unknown")
|
TARGET_SHA=$(git -C "$WEBSITE_DIR" rev-parse origin/master 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
if [[ "$CURRENT_SHA" != "unknown" && "$CURRENT_SHA" == "$TARGET_SHA" ]]; then
|
if [[ "$CURRENT_SHA" != "unknown" && "$CURRENT_SHA" == "$TARGET_SHA" ]]; then
|
||||||
echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$CURRENT_SHA\",\"status_message\":\"Website is already up to date with origin/master ($CURRENT_SHA).\"}"
|
echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$CURRENT_SHA\",\"status_message\":\"Website is already up to date with origin/master ($CURRENT_SHA).\"}"
|
||||||
exit 0
|
exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Stage new build release
|
# 2. Restart website service to activate deployed runtime
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
if ! restart_website_service; then
|
||||||
NEW_RELEASE="/opt/motorsignal/releases/${TIMESTAMP}-website"
|
echo '{"status":"failed","failure_code":"service_restart_failed","status_message":"Failed to restart motorsignal-website.service."}'
|
||||||
PREV_RELEASE=$(readlink -f "$CURRENT_LINK" 2>/dev/null || echo "")
|
exit 1
|
||||||
|
|
||||||
install -d -o motorsignal -g motorsignal "$NEW_RELEASE"
|
|
||||||
|
|
||||||
# Copy current website runtime into staging directory
|
|
||||||
if [[ -d "$WEBSITE_DIR" ]]; then
|
|
||||||
cp -a "$WEBSITE_DIR" "$NEW_RELEASE/website"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run production build in staging directory
|
# 3. Verification Gate (Loopback & Public)
|
||||||
if [[ -f "$NEW_RELEASE/website/package.json" ]]; then
|
|
||||||
cd "$NEW_RELEASE/website"
|
|
||||||
export NODE_ENV=production
|
|
||||||
if command -v npm >/dev/null 2>&1; then
|
|
||||||
npm run build --quiet || {
|
|
||||||
echo '{"status":"failed","failure_code":"build_failed","status_message":"Next.js production build failed."}'
|
|
||||||
rm -rf "$NEW_RELEASE"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Atomic Symlink Cutover
|
|
||||||
ln -sfn "$NEW_RELEASE" "/opt/motorsignal/current.next"
|
|
||||||
mv -Tf "/opt/motorsignal/current.next" "$CURRENT_LINK"
|
|
||||||
|
|
||||||
# 4. Restart website service
|
|
||||||
systemctl restart motorsignal-website.service
|
|
||||||
|
|
||||||
# 5. Verification Gate (Loopback & Public)
|
|
||||||
sleep 2
|
sleep 2
|
||||||
VERIFY_OK=0
|
VERIFY_OK=0
|
||||||
if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then
|
if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then
|
||||||
|
|
@ -84,16 +77,13 @@ case "$ACTION" in
|
||||||
if [[ "$VERIFY_OK" -eq 1 ]]; then
|
if [[ "$VERIFY_OK" -eq 1 ]]; then
|
||||||
VER_JSON=$(curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" 2>/dev/null || echo '{"revision":"unknown"}')
|
VER_JSON=$(curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" 2>/dev/null || echo '{"revision":"unknown"}')
|
||||||
DEPLOYED_SHA=$(echo "$VER_JSON" | grep -o '"revision":"[^"]*' | cut -d'"' -f4 || echo "unknown")
|
DEPLOYED_SHA=$(echo "$VER_JSON" | grep -o '"revision":"[^"]*' | cut -d'"' -f4 || echo "unknown")
|
||||||
echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$DEPLOYED_SHA\",\"status_message\":\"Website deployed and verified successfully.\"}"
|
echo "{\"status\":\"succeeded\",\"deployed_revision\":\"$DEPLOYED_SHA\",\"status_message\":\"Website deployment active and health verified on revision $DEPLOYED_SHA.\"}"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
# Automatic Rollback Gate
|
# Automatic Rollback Gate
|
||||||
echo ">>> [OPS-HELPER] Health check failed! Rolling back to previous release..."
|
echo ">>> [OPS-HELPER] Health check failed! Rolling back to previous release..."
|
||||||
if [[ -n "$PREV_RELEASE" && -d "$PREV_RELEASE" ]]; then
|
if [[ -n "$PREV_RELEASE" && -d "$PREV_RELEASE" ]]; then
|
||||||
ln -sfn "$PREV_RELEASE" "/opt/motorsignal/current.next"
|
restart_website_service || true
|
||||||
mv -Tf "/opt/motorsignal/current.next" "$CURRENT_LINK"
|
|
||||||
systemctl restart motorsignal-website.service
|
|
||||||
sleep 2
|
|
||||||
echo '{"status":"rolled_back","rollback_status":"rolled_back","failure_code":"health_check_failed","status_message":"Website deployment health check failed; restored previous release."}'
|
echo '{"status":"rolled_back","rollback_status":"rolled_back","failure_code":"health_check_failed","status_message":"Website deployment health check failed; restored previous release."}'
|
||||||
else
|
else
|
||||||
echo '{"status":"failed","failure_code":"rollback_failed","status_message":"Website deployment health check failed and no previous release was available for rollback."}'
|
echo '{"status":"failed","failure_code":"rollback_failed","status_message":"Website deployment health check failed and no previous release was available for rollback."}'
|
||||||
|
|
@ -104,7 +94,10 @@ case "$ACTION" in
|
||||||
|
|
||||||
restart_website)
|
restart_website)
|
||||||
echo ">>> [OPS-HELPER] Restarting motorsignal-website..."
|
echo ">>> [OPS-HELPER] Restarting motorsignal-website..."
|
||||||
systemctl restart motorsignal-website.service
|
if ! restart_website_service; then
|
||||||
|
echo '{"status":"failed","failure_code":"service_restart_failed","status_message":"Failed to restart motorsignal-website.service."}'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then
|
if curl -fsS --max-time 5 "$INTERNAL_WEBSITE_URL" >/dev/null 2>&1; then
|
||||||
|
|
@ -118,11 +111,13 @@ case "$ACTION" in
|
||||||
|
|
||||||
restart_api)
|
restart_api)
|
||||||
echo ">>> [OPS-HELPER] Scheduling detached API restart..."
|
echo ">>> [OPS-HELPER] Scheduling detached API restart..."
|
||||||
# Launch detached via systemd-run so parent process exits cleanly before restart
|
|
||||||
systemd-run --unit="motorsignal-api-restart-$(date +%s)" \
|
systemd-run --unit="motorsignal-api-restart-$(date +%s)" \
|
||||||
--description="MotorSignal API Delayed Restart" \
|
--description="MotorSignal API Delayed Restart" \
|
||||||
bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 || \
|
bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 || \
|
||||||
(nohup bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 &)
|
sudo -n systemd-run --unit="motorsignal-api-restart-$(date +%s)" \
|
||||||
|
--description="MotorSignal API Delayed Restart" \
|
||||||
|
bash -c 'sleep 2; systemctl restart motorsignal-api.service' >/dev/null 2>&1 || \
|
||||||
|
(nohup bash -c 'sleep 2; sudo -n systemctl restart motorsignal-api.service || systemctl restart motorsignal-api.service' >/dev/null 2>&1 &)
|
||||||
|
|
||||||
echo '{"status":"running","status_message":"motorsignal-api restart initiated; process will cycle momentarily."}'
|
echo '{"status":"running","status_message":"motorsignal-api restart initiated; process will cycle momentarily."}'
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -133,7 +128,10 @@ case "$ACTION" in
|
||||||
systemd-run --unit="motorsignal-server-reboot-$(date +%s)" \
|
systemd-run --unit="motorsignal-server-reboot-$(date +%s)" \
|
||||||
--description="MotorSignal Server Delayed Reboot" \
|
--description="MotorSignal Server Delayed Reboot" \
|
||||||
bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 || \
|
bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 || \
|
||||||
(nohup bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 &)
|
sudo -n systemd-run --unit="motorsignal-server-reboot-$(date +%s)" \
|
||||||
|
--description="MotorSignal Server Delayed Reboot" \
|
||||||
|
bash -c 'sleep 3; systemctl reboot' >/dev/null 2>&1 || \
|
||||||
|
(nohup bash -c 'sleep 3; sudo -n systemctl reboot || systemctl reboot' >/dev/null 2>&1 &)
|
||||||
|
|
||||||
echo '{"status":"running","status_message":"Server reboot initiated; system will restart in 3 seconds."}'
|
echo '{"status":"running","status_message":"Server reboot initiated; system will restart in 3 seconds."}'
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1407,6 +1407,30 @@ assert.ok(actionsComponentSrc.includes('Actor'), 'History table must display Act
|
||||||
assert.ok(actionsComponentSrc.includes('Status'), 'History table must display Status column');
|
assert.ok(actionsComponentSrc.includes('Status'), 'History table must display Status column');
|
||||||
assert.ok(actionsComponentSrc.includes('Revision'), 'History table must display Revision column');
|
assert.ok(actionsComponentSrc.includes('Revision'), 'History table must display Revision column');
|
||||||
|
|
||||||
|
// 44.1b Live Progress Tracking & Completion State (Phase 2 UI Tracking)
|
||||||
|
assert.ok(
|
||||||
|
actionsComponentSrc.includes('DEPLOY_PHASES') &&
|
||||||
|
actionsComponentSrc.includes('Preflight Checks') &&
|
||||||
|
actionsComponentSrc.includes('Release Staging') &&
|
||||||
|
actionsComponentSrc.includes('Production Build') &&
|
||||||
|
actionsComponentSrc.includes('Health Verification'),
|
||||||
|
'Actions component must define multi-phase deployment stepper'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
actionsComponentSrc.includes('formatElapsed') && actionsComponentSrc.includes('elapsedSeconds'),
|
||||||
|
'Actions component must track and display live elapsed execution time'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
actionsComponentSrc.includes('completedAction') &&
|
||||||
|
actionsComponentSrc.includes('modalMode') &&
|
||||||
|
actionsComponentSrc.includes('isMinimized'),
|
||||||
|
'Actions component must implement modal state transitions (confirm -> progress -> completed) with minimize'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
actionsComponentSrc.includes('recentBannerAction'),
|
||||||
|
'Actions component must display persistent completion banner after action execution'
|
||||||
|
);
|
||||||
|
|
||||||
// Embedded in Infrastructure view
|
// Embedded in Infrastructure view
|
||||||
const updatedInfraSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-infrastructure.tsx'), 'utf8');
|
const updatedInfraSrc = fs.readFileSync(path.resolve(__dirname, '../components/ops/operations-infrastructure.tsx'), 'utf8');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue