[grade=B urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq] DC-134/135/136: Shipdeck integration - data-driven login pages, deploy events, badge suppression

DC-134: /api/v1/auth/login-page serves a generic gated auto-login page for
any service registered in services.json without a curated flow (App Selector
installs, DC-131 git installs). Curated pages always win; unknown services
still 404; sanitizer keeps digits/hyphens (shipdeck-style ids); display
names HTML-escaped. Kills the sso-gate.js edit + restart per new install.

DC-135: shipdeck journal.jsonl tail worker (startShipdeckWorker) ingests
deploy/rollback lifecycle rows into the Security Center as
source_type=shipdeck (notice/success, error/failure via verify[] block).

DC-136: deploy-aware badge suppression - suppressDuringDeploy() before the
bridge call in /deploy and /rollback, clearDeploySuppression() in finally
(every exit path incl. rejected fetches), reference-counted for overlapping
deploys, 10-min TTL auto-expiry (HEALTH_DEPLOY_SUPPRESS_MAX_MS).

23 new tests across 4 suites; full suite 2923/2923 green.

Codex judge: C (r1) -> C (r2) -> B (r3) -> B zero-blockers (r4,
urn:ump:3wvdd3yegylr7e2pzn6tebyq5bisf2awmss7p4j3uwpwsujtnwoq).
This commit is contained in:
DashCaddy Polish Loop
2026-09-16 03:10:54 -07:00
parent 0dd8493f98
commit 11c719e635
10 changed files with 912 additions and 11 deletions
+34 -3
View File
@@ -40,7 +40,7 @@ function readBridgeToken() {
}
}
module.exports = function ({ asyncHandler, log, auditLogger, fetchT }) {
module.exports = function ({ asyncHandler, log, auditLogger, fetchT, healthChecker }) {
const router = express.Router();
function featureEnabled() {
@@ -151,8 +151,29 @@ module.exports = function ({ asyncHandler, log, auditLogger, fetchT }) {
if (typeof dir !== 'string' || !dir.trim()) {
return errorResponse(res, 400, 'dir is required');
}
const serviceNames = (() => {
// The deploy dir may host a differently-named service; suppress the
// dir basename plus whatever service name the panel passed alongside
// the request (deploy payload convention), covering naming skew.
const base = String(dir).replace(/\/+$/, '').split('/').pop() || '';
const requested = typeof (req.body && req.body.service) === 'string' ? req.body.service : '';
return [...new Set([base, requested])].filter(n => n && SERVICE_RE.test(n));
})();
try {
const { status, body } = await bridge('POST', '/api/deploy', { dir }, SHIPDECK_DEPLOY_TIMEOUT);
// DC-136: suppress BEFORE initiating — the restart blackholes probes
// DURING the bridge call, not after it resolves.
if (healthChecker) serviceNames.forEach(n => healthChecker.suppressDuringDeploy(n));
let bridgeResult;
try {
bridgeResult = await bridge('POST', '/api/deploy', { dir }, SHIPDECK_DEPLOY_TIMEOUT);
} finally {
// Judge r3: guaranteed cleanup on EVERY exit path — success, HTTP
// failure, thrown fetch error. A failed deploy never leaves real
// downtime hidden behind a suppression window.
if (healthChecker) serviceNames.forEach(n => healthChecker.clearDeploySuppression(n));
}
const { status, body } = bridgeResult;
if (auditLogger) {
auditLogger.log({
action: 'deploy.shipdeck',
@@ -179,7 +200,17 @@ module.exports = function ({ asyncHandler, log, auditLogger, fetchT }) {
return errorResponse(res, 400, 'invalid service name');
}
try {
const { status, body } = await bridge('POST', '/api/rollback', { service }, SHIPDECK_DEPLOY_TIMEOUT);
// DC-136: suppress BEFORE the rollback restarts the unit (same shape
// as /deploy); clear in a finally so failed rollbacks and thrown
// bridge errors never hide real downtime.
if (healthChecker) healthChecker.suppressDuringDeploy(service);
let bridgeResult;
try {
bridgeResult = await bridge('POST', '/api/rollback', { service }, SHIPDECK_DEPLOY_TIMEOUT);
} finally {
if (healthChecker) healthChecker.clearDeploySuppression(service);
}
const { status, body } = bridgeResult;
if (auditLogger) {
auditLogger.log({
action: 'deploy.rollback',