devops-whc 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_MCP_USAGE.md +394 -0
- package/LICENSE +15 -0
- package/README.md +208 -0
- package/WHC_MCP_REQUIREMENTS.md +112 -0
- package/dist/audit/audit-logger.js +57 -0
- package/dist/clients/ssh-client.js +199 -0
- package/dist/clients/whc-uapi-client.js +178 -0
- package/dist/clients/wpcli-client.js +125 -0
- package/dist/config/env.js +132 -0
- package/dist/contracts/deployment.js +2 -0
- package/dist/contracts/envelope.js +2 -0
- package/dist/dispatcher/tool-dispatcher.js +145 -0
- package/dist/handlers/whc-check-health.js +131 -0
- package/dist/handlers/whc-db-backup.js +111 -0
- package/dist/handlers/whc-deploy.js +381 -0
- package/dist/handlers/whc-get-logs.js +108 -0
- package/dist/handlers/whc-pipeline-status.js +96 -0
- package/dist/handlers/whc-prepare.js +127 -0
- package/dist/handlers/whc-rollback.js +141 -0
- package/dist/handlers/whc-setup-remote.js +262 -0
- package/dist/handlers/whc-ssh-exec.js +138 -0
- package/dist/handlers/whc-verify.js +304 -0
- package/dist/idempotency/store.js +13 -0
- package/dist/index.js +109 -0
- package/dist/policy/policy-engine.js +41 -0
- package/dist/probes/connectivity.js +41 -0
- package/dist/registry/tool-registry.js +69 -0
- package/dist/schemas/whc-check-health.js +55 -0
- package/dist/schemas/whc-db-backup.js +29 -0
- package/dist/schemas/whc-deploy.js +66 -0
- package/dist/schemas/whc-get-logs.js +25 -0
- package/dist/schemas/whc-pipeline-status.js +24 -0
- package/dist/schemas/whc-prepare.js +29 -0
- package/dist/schemas/whc-rollback.js +58 -0
- package/dist/schemas/whc-setup-remote.js +60 -0
- package/dist/schemas/whc-ssh-exec.js +117 -0
- package/dist/schemas/whc-verify.js +28 -0
- package/dist/server-entry.js +8 -0
- package/dist/server.js +381 -0
- package/dist/services/deploy-runtime-ops.js +104 -0
- package/dist/services/deployment-locks.js +34 -0
- package/dist/state/workspace-state.js +201 -0
- package/package.json +48 -0
- package/scripts/prepare-first-time.cjs +75 -0
- package/scripts/start-mcp.cjs +42 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
const zod_1 = require("zod");
|
|
9
|
+
if (process.env.WHC_ENV_FILE && process.env.WHC_ENV_FILE.trim().length > 0) {
|
|
10
|
+
dotenv_1.default.config({ path: process.env.WHC_ENV_FILE });
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
dotenv_1.default.config();
|
|
14
|
+
}
|
|
15
|
+
const envSchema = zod_1.z.object({
|
|
16
|
+
WHC_API_TOKEN: zod_1.z.string().min(1),
|
|
17
|
+
WHC_USER: zod_1.z.string().min(1),
|
|
18
|
+
WHC_HOST: zod_1.z.string().min(1),
|
|
19
|
+
WHC_WORKFLOW_MODE: zod_1.z.enum(["managed_clone_sync", "git_controlled"]).default("managed_clone_sync"),
|
|
20
|
+
WHC_PRIMARY_DOMAIN: zod_1.z.string().min(1).optional(),
|
|
21
|
+
WHC_TESTING_DOMAIN: zod_1.z.string().min(1).optional(),
|
|
22
|
+
WHC_STAGING_DOMAIN: zod_1.z.string().min(1).optional(),
|
|
23
|
+
WHC_PROD_PATH: zod_1.z.string().min(1).default("/public_html"),
|
|
24
|
+
WHC_STAGING_PATH: zod_1.z.string().min(1).optional(),
|
|
25
|
+
WHC_REQUIRE_STAGING_CONFIRM: zod_1.z
|
|
26
|
+
.enum(["true", "false"])
|
|
27
|
+
.default("true")
|
|
28
|
+
.transform((value) => value === "true"),
|
|
29
|
+
WHC_WARN_DYNAMIC_DATA_SYNC: zod_1.z
|
|
30
|
+
.enum(["true", "false"])
|
|
31
|
+
.default("true")
|
|
32
|
+
.transform((value) => value === "true"),
|
|
33
|
+
WHC_ENFORCE_STAGING_FIRST: zod_1.z
|
|
34
|
+
.enum(["true", "false"])
|
|
35
|
+
.default("true")
|
|
36
|
+
.transform((value) => value === "true"),
|
|
37
|
+
WHC_ALLOW_STAGING_GIT_CONTROLLED: zod_1.z
|
|
38
|
+
.enum(["true", "false"])
|
|
39
|
+
.default("false")
|
|
40
|
+
.transform((value) => value === "true"),
|
|
41
|
+
WHC_PROD_SSH_HOST: zod_1.z.string().min(1),
|
|
42
|
+
WHC_PROD_SSH_PORT: zod_1.z.coerce.number().int().positive().default(27),
|
|
43
|
+
WHC_PROD_SSH_USERNAME: zod_1.z.string().min(1),
|
|
44
|
+
WHC_PROD_SSH_PRIVATE_KEY_PATH: zod_1.z.string().min(1),
|
|
45
|
+
WHC_STAGING_SSH_HOST: zod_1.z.string().min(1).optional(),
|
|
46
|
+
WHC_STAGING_SSH_PORT: zod_1.z.coerce.number().int().positive().optional(),
|
|
47
|
+
WHC_STAGING_SSH_USERNAME: zod_1.z.string().min(1).optional(),
|
|
48
|
+
WHC_STAGING_SSH_PRIVATE_KEY_PATH: zod_1.z.string().min(1).optional(),
|
|
49
|
+
WHC_STAGING_SSH_PASSWORD: zod_1.z.string().min(1).optional(),
|
|
50
|
+
WHC_STAGING_SSH_HOSTKEY: zod_1.z.string().min(1).optional(),
|
|
51
|
+
WHC_LOCAL_PROJECT_ROOT: zod_1.z.string().min(1).optional(),
|
|
52
|
+
WHC_LOCAL_APP_PATH: zod_1.z.string().min(1).optional(),
|
|
53
|
+
WHC_SOURCE_KIND: zod_1.z
|
|
54
|
+
.enum(["full_site", "partial_content", "package_only", "artifact_first", "monorepo_slice"])
|
|
55
|
+
.default("full_site"),
|
|
56
|
+
WHC_DEPLOY_UNIT: zod_1.z.enum(["raw_source", "build_artifact", "package_bundle"]).default("raw_source"),
|
|
57
|
+
WHC_BUILD_COMMAND: zod_1.z.string().min(1).optional(),
|
|
58
|
+
WHC_BUILD_ARTIFACT_PATH: zod_1.z.string().min(1).optional(),
|
|
59
|
+
WHC_DEFAULT_RELEASE_INTENT: zod_1.z
|
|
60
|
+
.enum(["refresh", "deploy", "promote", "migrate", "recover"])
|
|
61
|
+
.default("deploy"),
|
|
62
|
+
WHC_API_TIMEOUT_MS: zod_1.z.coerce.number().int().positive().default(15000),
|
|
63
|
+
WHC_SSH_TIMEOUT_MS: zod_1.z.coerce.number().int().positive().default(10000),
|
|
64
|
+
WHC_FLOW_LOG_PATH: zod_1.z.string().min(1).optional(),
|
|
65
|
+
});
|
|
66
|
+
function loadConfig(env = process.env) {
|
|
67
|
+
const parsed = envSchema.safeParse(env);
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
const issues = parsed.error.issues
|
|
70
|
+
.map((issue) => issue.path.join("."))
|
|
71
|
+
.filter((field) => field.length > 0);
|
|
72
|
+
throw new Error(`VALIDATION_ERROR: Missing or invalid env vars: ${issues.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
const data = parsed.data;
|
|
75
|
+
const prodSshHost = data.WHC_PROD_SSH_HOST;
|
|
76
|
+
const prodSshPort = data.WHC_PROD_SSH_PORT;
|
|
77
|
+
const prodSshUsername = data.WHC_PROD_SSH_USERNAME;
|
|
78
|
+
const prodSshPrivateKeyPath = data.WHC_PROD_SSH_PRIVATE_KEY_PATH;
|
|
79
|
+
const hasStagingSsh = !!data.WHC_STAGING_SSH_HOST || !!data.WHC_STAGING_SSH_USERNAME || !!data.WHC_STAGING_SSH_PRIVATE_KEY_PATH;
|
|
80
|
+
return {
|
|
81
|
+
apiToken: data.WHC_API_TOKEN,
|
|
82
|
+
user: data.WHC_USER,
|
|
83
|
+
host: data.WHC_HOST,
|
|
84
|
+
apiBaseUrl: `https://${data.WHC_HOST}:2083`,
|
|
85
|
+
workflowMode: data.WHC_WORKFLOW_MODE,
|
|
86
|
+
domains: {
|
|
87
|
+
primary: data.WHC_PRIMARY_DOMAIN,
|
|
88
|
+
testing: data.WHC_TESTING_DOMAIN,
|
|
89
|
+
staging: data.WHC_STAGING_DOMAIN,
|
|
90
|
+
},
|
|
91
|
+
paths: {
|
|
92
|
+
prod: data.WHC_PROD_PATH,
|
|
93
|
+
staging: data.WHC_STAGING_PATH,
|
|
94
|
+
},
|
|
95
|
+
sourceProfile: {
|
|
96
|
+
localProjectRoot: data.WHC_LOCAL_PROJECT_ROOT,
|
|
97
|
+
localAppPath: data.WHC_LOCAL_APP_PATH,
|
|
98
|
+
sourceKind: data.WHC_SOURCE_KIND,
|
|
99
|
+
deployUnit: data.WHC_DEPLOY_UNIT,
|
|
100
|
+
buildCommand: data.WHC_BUILD_COMMAND,
|
|
101
|
+
buildArtifactPath: data.WHC_BUILD_ARTIFACT_PATH,
|
|
102
|
+
defaultReleaseIntent: data.WHC_DEFAULT_RELEASE_INTENT,
|
|
103
|
+
},
|
|
104
|
+
safety: {
|
|
105
|
+
requireStagingConfirm: data.WHC_REQUIRE_STAGING_CONFIRM,
|
|
106
|
+
warnDynamicDataSync: data.WHC_WARN_DYNAMIC_DATA_SYNC,
|
|
107
|
+
enforceStagingFirst: data.WHC_ENFORCE_STAGING_FIRST,
|
|
108
|
+
allowStagingGitControlled: data.WHC_ALLOW_STAGING_GIT_CONTROLLED,
|
|
109
|
+
},
|
|
110
|
+
sshTargets: {
|
|
111
|
+
prod: {
|
|
112
|
+
host: prodSshHost,
|
|
113
|
+
port: prodSshPort,
|
|
114
|
+
username: prodSshUsername,
|
|
115
|
+
privateKeyPath: prodSshPrivateKeyPath,
|
|
116
|
+
},
|
|
117
|
+
staging: hasStagingSsh
|
|
118
|
+
? {
|
|
119
|
+
host: data.WHC_STAGING_SSH_HOST ?? prodSshHost,
|
|
120
|
+
port: data.WHC_STAGING_SSH_PORT ?? prodSshPort,
|
|
121
|
+
username: data.WHC_STAGING_SSH_USERNAME ?? prodSshUsername,
|
|
122
|
+
privateKeyPath: data.WHC_STAGING_SSH_PRIVATE_KEY_PATH,
|
|
123
|
+
password: data.WHC_STAGING_SSH_PASSWORD,
|
|
124
|
+
hostKey: data.WHC_STAGING_SSH_HOSTKEY,
|
|
125
|
+
}
|
|
126
|
+
: undefined,
|
|
127
|
+
},
|
|
128
|
+
apiTimeoutMs: data.WHC_API_TIMEOUT_MS,
|
|
129
|
+
sshTimeoutMs: data.WHC_SSH_TIMEOUT_MS,
|
|
130
|
+
flowLogPath: data.WHC_FLOW_LOG_PATH,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dispatch = dispatch;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_crypto_2 = require("node:crypto");
|
|
6
|
+
const policy_engine_1 = require("../policy/policy-engine");
|
|
7
|
+
const workspace_state_1 = require("../state/workspace-state");
|
|
8
|
+
async function dispatch(options) {
|
|
9
|
+
const { config, toolDef, rawInput, validate, execute, idempotencyStore, auditLogger, actionIdFactory = node_crypto_2.randomUUID, } = options;
|
|
10
|
+
const startedAt = Date.now();
|
|
11
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
12
|
+
// Step 1: Validate input schema
|
|
13
|
+
let request;
|
|
14
|
+
try {
|
|
15
|
+
request = validate(rawInput);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const message = error instanceof Error ? error.message : "Unknown validation error";
|
|
19
|
+
return buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
|
|
20
|
+
code: "VALIDATION_ERROR",
|
|
21
|
+
message,
|
|
22
|
+
retryable: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
// Step 2: Policy evaluation
|
|
26
|
+
const policy = (0, policy_engine_1.evaluatePolicy)({
|
|
27
|
+
toolName: toolDef.name,
|
|
28
|
+
mode: toolDef.mode,
|
|
29
|
+
safetyLevel: toolDef.safetyLevel,
|
|
30
|
+
dryRun: request.dry_run,
|
|
31
|
+
confirmed: request.confirmed,
|
|
32
|
+
});
|
|
33
|
+
if (!policy.allowed) {
|
|
34
|
+
return buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
|
|
35
|
+
code: "BUSINESS_POLICY_BLOCKED",
|
|
36
|
+
message: policy.reason ?? "Operation blocked by safety policy",
|
|
37
|
+
retryable: false,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Step 3: Idempotency check (write operations only)
|
|
41
|
+
let idempotencyKey;
|
|
42
|
+
let payloadHash;
|
|
43
|
+
if (toolDef.mode === "write" && request.idempotency_key) {
|
|
44
|
+
idempotencyKey = request.idempotency_key;
|
|
45
|
+
payloadHash = hashPayload(request.payload);
|
|
46
|
+
const existing = idempotencyStore.get(idempotencyKey);
|
|
47
|
+
if (existing && existing.payloadHash === payloadHash) {
|
|
48
|
+
return existing.response;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Step 4: Execute handler
|
|
52
|
+
let response;
|
|
53
|
+
if (toolDef.mode === "write" && process.env.NODE_ENV !== "test") {
|
|
54
|
+
(0, workspace_state_1.writePipelineStartState)({
|
|
55
|
+
rootDir: process.cwd(),
|
|
56
|
+
toolName: toolDef.name,
|
|
57
|
+
requestId: request.request_id,
|
|
58
|
+
targetEnvironment: extractStringField(request.payload, "target_environment"),
|
|
59
|
+
pipelineId: extractStringField(request.payload, "pipeline_id"),
|
|
60
|
+
releaseIntent: extractStringField(request.payload, "release_intent"),
|
|
61
|
+
startedAtIso,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
response = await execute(config, request);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : "Unknown execution error";
|
|
69
|
+
response = buildErrorEnvelope(toolDef.name, actionIdFactory(), startedAt, toolDef.safetyLevel, {
|
|
70
|
+
code: "UNKNOWN_UPSTREAM_ERROR",
|
|
71
|
+
message,
|
|
72
|
+
retryable: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (toolDef.mode === "write" && process.env.NODE_ENV !== "test") {
|
|
76
|
+
const data = (response.data ?? null);
|
|
77
|
+
const processTracking = data && typeof data === "object" ? data["process_tracking"] : undefined;
|
|
78
|
+
const nextStep = processTracking && typeof processTracking["next_step"] === "string" ? processTracking["next_step"] : undefined;
|
|
79
|
+
(0, workspace_state_1.writePipelineEndState)({
|
|
80
|
+
rootDir: process.cwd(),
|
|
81
|
+
toolName: toolDef.name,
|
|
82
|
+
requestId: request.request_id,
|
|
83
|
+
targetEnvironment: extractStringField(request.payload, "target_environment"),
|
|
84
|
+
pipelineId: extractStringField(request.payload, "pipeline_id"),
|
|
85
|
+
releaseIntent: extractStringField(request.payload, "release_intent"),
|
|
86
|
+
startedAtIso,
|
|
87
|
+
}, {
|
|
88
|
+
ok: response.ok,
|
|
89
|
+
actionId: response.action_id,
|
|
90
|
+
nextStep,
|
|
91
|
+
errorCode: response.error?.code,
|
|
92
|
+
errorMessage: response.error?.message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Step 5: Audit write operations
|
|
96
|
+
if (toolDef.mode === "write") {
|
|
97
|
+
auditLogger.log({
|
|
98
|
+
actionId: response.action_id,
|
|
99
|
+
tool: toolDef.name,
|
|
100
|
+
status: response.ok ? "success" : "failure",
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
details: {
|
|
103
|
+
dry_run: request.dry_run ?? false,
|
|
104
|
+
ok: response.ok,
|
|
105
|
+
idempotency_key: idempotencyKey ?? "",
|
|
106
|
+
request_id: request.request_id,
|
|
107
|
+
target_environment: extractStringField(request.payload, "target_environment"),
|
|
108
|
+
pipeline_id: extractStringField(request.payload, "pipeline_id"),
|
|
109
|
+
release_intent: extractStringField(request.payload, "release_intent"),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Step 6: Store idempotency record for successful writes
|
|
114
|
+
if (toolDef.mode === "write" && idempotencyKey && payloadHash && response.ok) {
|
|
115
|
+
idempotencyStore.set(idempotencyKey, {
|
|
116
|
+
actionId: response.action_id,
|
|
117
|
+
payloadHash,
|
|
118
|
+
response,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return response;
|
|
122
|
+
}
|
|
123
|
+
function buildErrorEnvelope(tool, actionId, startedAt, safetyLevel, error) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
action_id: actionId,
|
|
127
|
+
tool,
|
|
128
|
+
data: null,
|
|
129
|
+
error,
|
|
130
|
+
meta: {
|
|
131
|
+
latency_ms: Date.now() - startedAt,
|
|
132
|
+
safety_level: safetyLevel,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function hashPayload(payload) {
|
|
137
|
+
return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
138
|
+
}
|
|
139
|
+
function extractStringField(payload, key) {
|
|
140
|
+
if (!payload || typeof payload !== "object") {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
const value = payload[key];
|
|
144
|
+
return typeof value === "string" ? value : "";
|
|
145
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWhcCheckHealth = executeWhcCheckHealth;
|
|
4
|
+
exports.handleWhcCheckHealth = handleWhcCheckHealth;
|
|
5
|
+
exports.buildDefaultWhcCheckHealthRequest = buildDefaultWhcCheckHealthRequest;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
const connectivity_1 = require("../probes/connectivity");
|
|
8
|
+
const whc_check_health_1 = require("../schemas/whc-check-health");
|
|
9
|
+
/**
|
|
10
|
+
* Pure executor: assumes request is already validated.
|
|
11
|
+
* Used by the dispatcher (validate → policy → idempotency → execute → audit).
|
|
12
|
+
*/
|
|
13
|
+
async function executeWhcCheckHealth(config, request, deps = {}) {
|
|
14
|
+
const startedAt = Date.now();
|
|
15
|
+
const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
|
|
16
|
+
const probeRunner = deps.probeRunner ?? connectivity_1.runConnectivityProbe;
|
|
17
|
+
try {
|
|
18
|
+
const report = await probeRunner(config);
|
|
19
|
+
const readiness = computeReadiness(report);
|
|
20
|
+
const warnings = buildWarnings(report);
|
|
21
|
+
return {
|
|
22
|
+
ok: true,
|
|
23
|
+
action_id: actionIdFactory(),
|
|
24
|
+
tool: "whc_check_health",
|
|
25
|
+
data: {
|
|
26
|
+
readiness,
|
|
27
|
+
capabilities: report,
|
|
28
|
+
paths: {
|
|
29
|
+
prod: config.paths.prod,
|
|
30
|
+
staging: config.paths.staging,
|
|
31
|
+
},
|
|
32
|
+
warnings,
|
|
33
|
+
},
|
|
34
|
+
error: null,
|
|
35
|
+
meta: {
|
|
36
|
+
latency_ms: Date.now() - startedAt,
|
|
37
|
+
safety_level: "A",
|
|
38
|
+
workflow_mode: config.workflowMode,
|
|
39
|
+
pipeline_id: request.payload.pipeline_id,
|
|
40
|
+
release_intent: request.payload.release_intent,
|
|
41
|
+
source_profile: request.payload.source_profile,
|
|
42
|
+
delivery_mechanism: "read_probe",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : "Unknown health-check error";
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
action_id: actionIdFactory(),
|
|
51
|
+
tool: "whc_check_health",
|
|
52
|
+
data: null,
|
|
53
|
+
error: {
|
|
54
|
+
code: "UNKNOWN_UPSTREAM_ERROR",
|
|
55
|
+
message,
|
|
56
|
+
retryable: true,
|
|
57
|
+
},
|
|
58
|
+
meta: {
|
|
59
|
+
latency_ms: Date.now() - startedAt,
|
|
60
|
+
safety_level: "A",
|
|
61
|
+
workflow_mode: config.workflowMode,
|
|
62
|
+
pipeline_id: request.payload.pipeline_id,
|
|
63
|
+
release_intent: request.payload.release_intent,
|
|
64
|
+
source_profile: request.payload.source_profile,
|
|
65
|
+
delivery_mechanism: "read_probe",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Standalone convenience wrapper: validates input then executes.
|
|
72
|
+
* Used for direct CLI invocation without the dispatcher.
|
|
73
|
+
*/
|
|
74
|
+
async function handleWhcCheckHealth(config, input, deps = {}) {
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
|
|
77
|
+
let request;
|
|
78
|
+
try {
|
|
79
|
+
request = (0, whc_check_health_1.validateWhcCheckHealthRequest)(input);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : "Unknown validation error";
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
action_id: actionIdFactory(),
|
|
86
|
+
tool: "whc_check_health",
|
|
87
|
+
data: null,
|
|
88
|
+
error: { code: "VALIDATION_ERROR", message, retryable: false },
|
|
89
|
+
meta: { latency_ms: Date.now() - startedAt, safety_level: "A" },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return executeWhcCheckHealth(config, request, deps);
|
|
93
|
+
}
|
|
94
|
+
function buildDefaultWhcCheckHealthRequest(config) {
|
|
95
|
+
return {
|
|
96
|
+
request_id: (0, node_crypto_1.randomUUID)(),
|
|
97
|
+
actor: {
|
|
98
|
+
kind: "copilot",
|
|
99
|
+
user_hint: "local-cli",
|
|
100
|
+
},
|
|
101
|
+
payload: (0, whc_check_health_1.buildDefaultWhcCheckHealthPayload)(config),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function computeReadiness(report) {
|
|
105
|
+
if (report.uapi.ok && report.ssh.ok) {
|
|
106
|
+
return "ready";
|
|
107
|
+
}
|
|
108
|
+
if (!report.uapi.ok || !report.ssh.ok) {
|
|
109
|
+
return "needs_fix";
|
|
110
|
+
}
|
|
111
|
+
return "unknown";
|
|
112
|
+
}
|
|
113
|
+
function buildWarnings(report) {
|
|
114
|
+
const warnings = [];
|
|
115
|
+
if (!report.uapi.ok) {
|
|
116
|
+
warnings.push(`UAPI connectivity issue: ${report.uapi.message}`);
|
|
117
|
+
}
|
|
118
|
+
if (!report.ssh.ok) {
|
|
119
|
+
warnings.push(`Production SSH issue: ${report.ssh.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (!report.sshEnvironments.staging.ok) {
|
|
122
|
+
warnings.push(`Staging SSH issue: ${report.sshEnvironments.staging.message}`);
|
|
123
|
+
}
|
|
124
|
+
if (!report.wpcli.prod.available) {
|
|
125
|
+
warnings.push(`WP-CLI unavailable on production: ${report.wpcli.prod.message}`);
|
|
126
|
+
}
|
|
127
|
+
if (!report.wpcli.staging.available) {
|
|
128
|
+
warnings.push(`WP-CLI unavailable on staging: ${report.wpcli.staging.message}`);
|
|
129
|
+
}
|
|
130
|
+
return warnings;
|
|
131
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWhcDbBackup = executeWhcDbBackup;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const ssh_client_1 = require("../clients/ssh-client");
|
|
6
|
+
function buildDumpPath(outputPath, env, compress) {
|
|
7
|
+
if (outputPath)
|
|
8
|
+
return outputPath;
|
|
9
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
10
|
+
const ext = compress ? ".sql.gz" : ".sql";
|
|
11
|
+
return `~/db-backups/${env}-${ts}${ext}`;
|
|
12
|
+
}
|
|
13
|
+
function buildWpCliCommand(dumpPath, compress, tables) {
|
|
14
|
+
const tableFlag = tables && tables.length > 0 ? ` --tables=${tables.join(",")}` : "";
|
|
15
|
+
const compressFlag = compress ? " | gzip" : "";
|
|
16
|
+
if (compress) {
|
|
17
|
+
return `wp db export -${tableFlag} 2>/dev/null${compressFlag} > ${dumpPath}`;
|
|
18
|
+
}
|
|
19
|
+
return `wp db export ${dumpPath}${tableFlag}`;
|
|
20
|
+
}
|
|
21
|
+
async function executeWhcDbBackup(config, request, deps = {}) {
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
const actionIdFactory = deps.actionIdFactory ?? node_crypto_1.randomUUID;
|
|
24
|
+
const sshClient = deps.sshClient ?? new ssh_client_1.WhcSshClient(config);
|
|
25
|
+
const { target_environment, output_path, compress, tables } = request.payload;
|
|
26
|
+
const dumpPath = buildDumpPath(output_path, target_environment, compress);
|
|
27
|
+
// Dry run: preview command without executing
|
|
28
|
+
if (request.dry_run) {
|
|
29
|
+
const previewCmd = buildWpCliCommand(dumpPath, compress, tables);
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
action_id: actionIdFactory(),
|
|
33
|
+
tool: "whc_db_backup",
|
|
34
|
+
data: {
|
|
35
|
+
target_environment,
|
|
36
|
+
dump_path: dumpPath,
|
|
37
|
+
compressed: compress,
|
|
38
|
+
tables,
|
|
39
|
+
},
|
|
40
|
+
error: null,
|
|
41
|
+
meta: {
|
|
42
|
+
latency_ms: Date.now() - startedAt,
|
|
43
|
+
safety_level: "D",
|
|
44
|
+
delivery_mechanism: "ssh_exec",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Resolve SSH target
|
|
49
|
+
let execResult;
|
|
50
|
+
const ensureDirCmd = `mkdir -p $(dirname ${dumpPath})`;
|
|
51
|
+
const backupCmd = buildWpCliCommand(dumpPath, compress, tables);
|
|
52
|
+
const sizeCmd = `du -sh ${dumpPath} 2>/dev/null | cut -f1 || echo 'unknown'`;
|
|
53
|
+
// Ensure backup dir exists, then run wp db export, then get size
|
|
54
|
+
const fullCommand = `${ensureDirCmd} && ${backupCmd} && ${sizeCmd}`;
|
|
55
|
+
try {
|
|
56
|
+
if (target_environment === "staging") {
|
|
57
|
+
const stagingTarget = config.sshTargets.staging;
|
|
58
|
+
if (!stagingTarget) {
|
|
59
|
+
return buildErrorResponse(actionIdFactory(), startedAt, "BUSINESS_POLICY_BLOCKED", "Staging SSH target is not configured.");
|
|
60
|
+
}
|
|
61
|
+
if (!stagingTarget.privateKeyPath) {
|
|
62
|
+
return buildErrorResponse(actionIdFactory(), startedAt, "BUSINESS_POLICY_BLOCKED", "whc_db_backup requires key-based SSH auth on staging.");
|
|
63
|
+
}
|
|
64
|
+
execResult = await sshClient.execWithKey({
|
|
65
|
+
host: stagingTarget.host,
|
|
66
|
+
port: stagingTarget.port,
|
|
67
|
+
username: stagingTarget.username,
|
|
68
|
+
privateKeyPath: stagingTarget.privateKeyPath,
|
|
69
|
+
}, fullCommand);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
execResult = await sshClient.execWithKey(config.sshTargets.prod, fullCommand);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : "SSH exec failed";
|
|
77
|
+
return buildErrorResponse(actionIdFactory(), startedAt, "AUTH_ERROR", message);
|
|
78
|
+
}
|
|
79
|
+
if (!execResult.ok) {
|
|
80
|
+
return buildErrorResponse(actionIdFactory(), startedAt, "TEMPORARY_UPSTREAM_ERROR", execResult.message || execResult.stderr || "wp db export failed");
|
|
81
|
+
}
|
|
82
|
+
const sizeHint = execResult.stdout.split("\n").filter((l) => l.trim().length > 0).pop()?.trim();
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
action_id: actionIdFactory(),
|
|
86
|
+
tool: "whc_db_backup",
|
|
87
|
+
data: {
|
|
88
|
+
target_environment,
|
|
89
|
+
dump_path: dumpPath,
|
|
90
|
+
compressed: compress,
|
|
91
|
+
tables,
|
|
92
|
+
size_hint: sizeHint,
|
|
93
|
+
},
|
|
94
|
+
error: null,
|
|
95
|
+
meta: {
|
|
96
|
+
latency_ms: Date.now() - startedAt,
|
|
97
|
+
safety_level: "D",
|
|
98
|
+
delivery_mechanism: "ssh_exec",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function buildErrorResponse(actionId, startedAt, code, message) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
action_id: actionId,
|
|
106
|
+
tool: "whc_db_backup",
|
|
107
|
+
data: null,
|
|
108
|
+
error: { code, message, retryable: code === "TEMPORARY_UPSTREAM_ERROR" },
|
|
109
|
+
meta: { latency_ms: Date.now() - startedAt, safety_level: "D" },
|
|
110
|
+
};
|
|
111
|
+
}
|