refactor: migrate v02 cloud api to TypeScript
This commit is contained in:
@@ -825,18 +825,43 @@ function shortRevision(value) {
|
||||
return source.length >= 7 ? source.slice(0, 7) : source || "unknown";
|
||||
}
|
||||
|
||||
function runNodeEntrypoint(file) {
|
||||
const child = spawn(process.execPath, [file], {
|
||||
function runEntrypoint(command, args) {
|
||||
const child = spawn(command, args, {
|
||||
cwd: "/app",
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
process.stderr.write(JSON.stringify({
|
||||
serviceId,
|
||||
status: "failed",
|
||||
error: "entrypoint_spawn_failed",
|
||||
command,
|
||||
reason: error.message
|
||||
}) + "\n");
|
||||
process.exit(127);
|
||||
});
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) process.kill(process.pid, signal);
|
||||
else process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
function runNodeEntrypoint(file) {
|
||||
runEntrypoint(process.execPath, [file]);
|
||||
}
|
||||
|
||||
function runBunEntrypoint(file) {
|
||||
const candidates = [
|
||||
process.env.HWLAB_BUN_COMMAND,
|
||||
"/app/node_modules/.bin/bun",
|
||||
"/usr/local/bin/bun",
|
||||
"bun"
|
||||
].filter(Boolean);
|
||||
const command = candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
|
||||
runEntrypoint(command, [file]);
|
||||
}
|
||||
|
||||
function cookieValue(request, name) {
|
||||
const raw = request.headers.cookie || "";
|
||||
for (const part of raw.split(";")) {
|
||||
@@ -1128,6 +1153,8 @@ function serveHealthOnly() {
|
||||
|
||||
if (runtimeKind === "node-command" && entrypoint) {
|
||||
runNodeEntrypoint(entrypoint);
|
||||
} else if (runtimeKind === "bun-command" && entrypoint) {
|
||||
runBunEntrypoint(entrypoint);
|
||||
} else if (runtimeKind === "cloud-web") {
|
||||
await serveCloudWeb();
|
||||
} else {
|
||||
@@ -1138,11 +1165,12 @@ if (runtimeKind === "node-command" && entrypoint) {
|
||||
}
|
||||
|
||||
function dockerfile(baseImage, port, labels = []) {
|
||||
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
|
||||
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
|
||||
return [
|
||||
`FROM ${baseImage}`,
|
||||
"WORKDIR /app",
|
||||
...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`),
|
||||
"ARG HWLAB_ARTIFACT_KIND",
|
||||
"ENV CODEX_HOME=/codex-home",
|
||||
"ENV HWLAB_CODE_AGENT_CODEX_HOME=/codex-home",
|
||||
"ENV HWLAB_CODE_AGENT_WORKSPACE=/workspace/hwlab",
|
||||
@@ -1167,7 +1195,6 @@ function dockerfile(baseImage, port, labels = []) {
|
||||
`RUN node -e "const fs=require('node:fs'); fs.writeFileSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', Buffer.from('${runtimeScriptBase64()}', 'base64')); fs.chmodSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', 0o755);"`,
|
||||
"ARG HWLAB_ENVIRONMENT",
|
||||
"ARG HWLAB_SERVICE_ID",
|
||||
"ARG HWLAB_ARTIFACT_KIND",
|
||||
"ARG HWLAB_SERVICE_ENTRYPOINT",
|
||||
"ARG HWLAB_COMMIT_ID",
|
||||
"ARG HWLAB_REVISION",
|
||||
@@ -1304,8 +1331,8 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
|
||||
blocker({
|
||||
type: "runtime_blocker",
|
||||
scope: service.serviceId,
|
||||
summary: `${service.serviceId} has no cmd/${service.serviceId}/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation.`,
|
||||
next: `Add cmd/${service.serviceId}/main.mjs or a dedicated Dockerfile for ${service.serviceId}.`
|
||||
summary: `${service.serviceId} has no runtime entrypoint; a health-only placeholder image was not built as a real implementation.`,
|
||||
next: `Add cmd/${service.serviceId}/main.mjs, cmd/${service.serviceId}/main.ts, or a dedicated Dockerfile for ${service.serviceId}.`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:net";
|
||||
|
||||
import { createCloudApiServer } from "../internal/cloud/server.mjs";
|
||||
import { createCloudApiServer } from "../internal/cloud/server.ts";
|
||||
import { ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
|
||||
const previousEnv = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import assert from "node:assert/strict";
|
||||
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
@@ -9,12 +9,12 @@ import path from "node:path";
|
||||
import {
|
||||
handleCodeAgentChat,
|
||||
validateCodeAgentChatSchema
|
||||
} from "../internal/cloud/code-agent-chat.mjs";
|
||||
} from "../internal/cloud/code-agent-chat.ts";
|
||||
import {
|
||||
codexAppServerArgs,
|
||||
codexAppServerProviderBaseUrl,
|
||||
createCodexStdioSessionManager
|
||||
} from "../internal/cloud/codex-stdio-session.mjs";
|
||||
} from "../internal/cloud/codex-stdio-session.ts";
|
||||
import {
|
||||
classifyCodexRunnerCapability,
|
||||
classifyCodeAgentChatReadiness,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
runDeployContractPlanCli,
|
||||
writeDeployContractPlanError
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
runDeployDesiredStatePlanCli,
|
||||
writeDeployDesiredStatePlanError
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
formatDevRuntimeMigrationFailure,
|
||||
runDevRuntimeMigrationCli
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import {
|
||||
formatDevRuntimeProvisioningFailure,
|
||||
runDevRuntimeProvisioningCli
|
||||
|
||||
@@ -67,7 +67,7 @@ test("component model uses built-in service paths", () => {
|
||||
"skills/",
|
||||
"tools/"
|
||||
]);
|
||||
assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.mjs"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.mjs"]);
|
||||
assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.ts"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.ts"]);
|
||||
});
|
||||
|
||||
test("planner remains compatible with deploy.k3s.serviceMappings shape", async () => {
|
||||
@@ -103,7 +103,7 @@ async function createFixtureRepo(options = {}) {
|
||||
}
|
||||
await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2));
|
||||
await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2));
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.mjs"), "console.log('api');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.mjs"), "console.log('test');\n");
|
||||
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html></html>\n");
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer } from "node:net";
|
||||
import { existsSync } from "node:fs";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import path from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -24,9 +26,10 @@ const commonEnv = {
|
||||
HWLAB_CLOUD_RUNTIME_DURABLE: "false"
|
||||
};
|
||||
const children = [];
|
||||
const bunCommand = resolveBunCommand();
|
||||
|
||||
try {
|
||||
const cloud = startNode("cmd/hwlab-cloud-api/main.mjs", {
|
||||
const cloud = startNode("cmd/hwlab-cloud-api/main.ts", {
|
||||
...commonEnv,
|
||||
HWLAB_CLOUD_API_HOST: "127.0.0.1",
|
||||
HWLAB_CLOUD_API_PORT: String(cloudPort),
|
||||
@@ -134,7 +137,8 @@ try {
|
||||
}
|
||||
|
||||
function startNode(entrypoint, env) {
|
||||
const child = spawn(process.execPath, [entrypoint], {
|
||||
const command = entrypoint.endsWith(".ts") ? bunCommand : process.execPath;
|
||||
const child = spawn(command, [entrypoint], {
|
||||
cwd,
|
||||
env,
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
@@ -153,6 +157,15 @@ function startNode(entrypoint, env) {
|
||||
return child;
|
||||
}
|
||||
|
||||
function resolveBunCommand() {
|
||||
const candidates = [
|
||||
process.env.HWLAB_BUN_COMMAND,
|
||||
process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null,
|
||||
"bun"
|
||||
].filter(Boolean);
|
||||
return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
|
||||
}
|
||||
|
||||
async function waitForGatewaySession(apiBaseUrl, gatewaySessionId) {
|
||||
const deadline = Date.now() + 8000;
|
||||
let last;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
@@ -8,11 +9,12 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
|
||||
import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.mjs";
|
||||
import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.ts";
|
||||
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const children = [];
|
||||
const bunCommand = resolveBunCommand();
|
||||
const CODEX_STDIO_FEASIBILITY_BLOCKERS = new Set([
|
||||
"codex_cli_binary_missing",
|
||||
"codex_stdio_supervisor_disabled",
|
||||
@@ -46,7 +48,8 @@ function freePort() {
|
||||
}
|
||||
|
||||
function startNode(relativeScript, env = {}) {
|
||||
const child = spawn(process.execPath, [relativeScript], {
|
||||
const command = relativeScript.endsWith(".ts") ? bunCommand : process.execPath;
|
||||
const child = spawn(command, [relativeScript], {
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -73,6 +76,15 @@ function startNode(relativeScript, env = {}) {
|
||||
return record;
|
||||
}
|
||||
|
||||
function resolveBunCommand() {
|
||||
const candidates = [
|
||||
process.env.HWLAB_BUN_COMMAND,
|
||||
process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null,
|
||||
"bun"
|
||||
].filter(Boolean);
|
||||
return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
|
||||
}
|
||||
|
||||
function runNodeJson(relativeScript, args = [], env = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [relativeScript, ...args], {
|
||||
@@ -184,7 +196,7 @@ function rpcEnvelope({ id, method, params = {} }) {
|
||||
|
||||
async function smokeCloudApi() {
|
||||
const port = await freePort();
|
||||
const service = startNode("cmd/hwlab-cloud-api/main.mjs", {
|
||||
const service = startNode("cmd/hwlab-cloud-api/main.ts", {
|
||||
HWLAB_CLOUD_API_HOST: "127.0.0.1",
|
||||
HWLAB_CLOUD_API_PORT: String(port),
|
||||
HWLAB_CLOUD_DB_URL: "",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
script: "run-bun",
|
||||
usage: "node scripts/run-bun.mjs <bun-args...>",
|
||||
env: "HWLAB_BUN_COMMAND",
|
||||
resolvedCommand: resolveBunCommand()
|
||||
}, null, 2) + "\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const command = resolveBunCommand();
|
||||
if (!command) {
|
||||
process.stderr.write(JSON.stringify({
|
||||
script: "run-bun",
|
||||
status: "failed",
|
||||
error: "bun_command_not_found",
|
||||
checked: bunCandidates()
|
||||
}, null, 2) + "\n");
|
||||
process.exit(127);
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
process.stderr.write(JSON.stringify({
|
||||
script: "run-bun",
|
||||
status: "failed",
|
||||
error: "bun_spawn_failed",
|
||||
command,
|
||||
reason: error.message
|
||||
}, null, 2) + "\n");
|
||||
process.exit(127);
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
process.kill(process.pid, signal);
|
||||
return;
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
|
||||
function resolveBunCommand() {
|
||||
for (const candidate of bunCandidates()) {
|
||||
if (!candidate) continue;
|
||||
if (!candidate.includes(path.sep)) return candidate;
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bunCandidates() {
|
||||
return [
|
||||
process.env.HWLAB_BUN_COMMAND,
|
||||
path.join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "bun.exe" : "bun"),
|
||||
path.join(os.homedir(), ".bun", "bin", process.platform === "win32" ? "bun.exe" : "bun"),
|
||||
"/root/.bun/bin/bun",
|
||||
"/usr/local/bin/bun",
|
||||
"bun"
|
||||
].filter(Boolean);
|
||||
}
|
||||
@@ -917,7 +917,7 @@ function summarizeCloudWebBuildFreshness(distFreshness, artifactCloudWeb) {
|
||||
async function inspectCodeAgentTimeoutReadiness(repoRoot) {
|
||||
const [appSource, cloudApiEntrySource] = await Promise.all([
|
||||
readFile(path.join(repoRoot, "web/hwlab-cloud-web/app.mjs"), "utf8"),
|
||||
readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.mjs"), "utf8")
|
||||
readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.ts"), "utf8")
|
||||
]);
|
||||
const frontendTimeoutMs = numericSourceConstant(appSource, "DEFAULT_CODE_AGENT_TIMEOUT_MS");
|
||||
const backendTimeoutMs = parseTimeoutFallbackMs(cloudApiEntrySource, "HWLAB_CODE_AGENT_TIMEOUT_MS");
|
||||
@@ -953,7 +953,7 @@ function parseTimeoutFallbackMs(source, envName) {
|
||||
async function inspectM3IoSourceReadiness(repoRoot) {
|
||||
const files = await listTrackedFiles(repoRoot);
|
||||
const candidateFiles = files.filter((file) =>
|
||||
/\.(?:mjs|js|json|md|html|css|yaml|yml)$/u.test(file) &&
|
||||
/\.(?:ts|mjs|js|json|md|html|css|yaml|yml)$/u.test(file) &&
|
||||
file !== "scripts/src/artifact-runtime-readiness-guard.mjs" &&
|
||||
file !== "scripts/artifact-runtime-readiness-guard.test.mjs"
|
||||
);
|
||||
@@ -971,7 +971,7 @@ async function inspectM3IoSourceReadiness(repoRoot) {
|
||||
const present = matches.length > 0;
|
||||
const sourceReady = contractFiles.length > 0 &&
|
||||
matches.some((file) => file === "web/hwlab-cloud-web/app.mjs") &&
|
||||
matches.some((file) => file === "internal/cloud/m3-io-control.mjs" || file === "scripts/src/m3-io-control-e2e.mjs");
|
||||
matches.some((file) => file === "internal/cloud/m3-io-control.ts" || file === "scripts/src/m3-io-control-e2e.mjs");
|
||||
return {
|
||||
status: present ? (sourceReady ? "pass" : "blocked") : "absent",
|
||||
present,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
codeAgentSecretRefPlaceholder
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
} from "../../internal/cloud/code-agent-contract.ts";
|
||||
import {
|
||||
HWLAB_M3_IO_API_BASE_URL_ENV,
|
||||
HWLAB_M3_IO_DEV_SERVICE_BASE_URL
|
||||
@@ -361,10 +361,10 @@ function validateCloudApiDbSource(ctx, env) {
|
||||
"cloud API DEV DB SSL mode"
|
||||
);
|
||||
expectEqual(ctx, env.HWLAB_CLOUD_DB_CONTRACT, "dev-redacted-presence-only", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_CONTRACT", "cloud API DB redacted contract marker");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "internal/cloud/db-contract.mjs.endpointAuthority.source", "cloud API DB runtime authority source");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "internal/cloud/db-contract.mjs.endpointAuthority.env", "cloud API DB runtime authority env");
|
||||
expectEqual(ctx, alias.requiredForReadiness, false, "internal/cloud/db-contract.mjs.dns.requiredForReadiness", "cloud API DB DNS alias readiness requirement");
|
||||
expectEqual(ctx, alias.usedForProbe, false, "internal/cloud/db-contract.mjs.dns.usedForProbe", "cloud API DB DNS alias probe source");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "internal/cloud/db-contract.ts.endpointAuthority.source", "cloud API DB runtime authority source");
|
||||
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "internal/cloud/db-contract.ts.endpointAuthority.env", "cloud API DB runtime authority env");
|
||||
expectEqual(ctx, alias.requiredForReadiness, false, "internal/cloud/db-contract.ts.dns.requiredForReadiness", "cloud API DB DNS alias readiness requirement");
|
||||
expectEqual(ctx, alias.usedForProbe, false, "internal/cloud/db-contract.ts.dns.usedForProbe", "cloud API DB DNS alias probe source");
|
||||
for (const name of [
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAME",
|
||||
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE",
|
||||
|
||||
@@ -7,8 +7,8 @@ import { promisify } from "node:util";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
inspectCodeAgentProviderManifestRefs
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
} from "../../internal/cloud/code-agent-contract.ts";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
|
||||
import { tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -63,6 +63,19 @@ function serviceSourceState(catalog, serviceId) {
|
||||
|
||||
export async function resolveDevArtifactService(repoRoot, serviceId, catalog = null) {
|
||||
const sourceState = serviceSourceState(catalog, serviceId);
|
||||
if (serviceId === "hwlab-cloud-api") {
|
||||
const tsCmdPath = `cmd/${serviceId}/main.ts`;
|
||||
if (await pathExists(repoRoot, tsCmdPath)) {
|
||||
return withServicePublishPolicy({
|
||||
serviceId,
|
||||
runtimeKind: "bun-command",
|
||||
entrypoint: tsCmdPath,
|
||||
implementationState: "repo-entrypoint",
|
||||
sourceState
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cmdPath = `cmd/${serviceId}/main.mjs`;
|
||||
if (await pathExists(repoRoot, cmdPath)) {
|
||||
return withServicePublishPolicy({
|
||||
|
||||
@@ -1640,7 +1640,7 @@ function readStaticFiles() {
|
||||
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
|
||||
help: readText("web/hwlab-cloud-web/help.md"),
|
||||
artifactPublisher: readText("scripts/artifact-publish.mjs"),
|
||||
cloudApiServer: readText("internal/cloud/server.mjs"),
|
||||
cloudApiServer: readText("internal/cloud/server.ts"),
|
||||
devicePodData: readText("internal/device-pod/fake-data.mjs"),
|
||||
devicePodService: readText("cmd/hwlab-device-pod/main.mjs"),
|
||||
protocol: readText("internal/protocol/index.mjs"),
|
||||
|
||||
@@ -8,11 +8,11 @@ import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
DEV_DB_ENV_CONTRACT,
|
||||
summarizeDbContract
|
||||
} from "../../internal/cloud/db-contract.mjs";
|
||||
} from "../../internal/cloud/db-contract.ts";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
codeAgentSecretRefPlaceholder
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
} from "../../internal/cloud/code-agent-contract.ts";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
@@ -187,7 +187,7 @@ async function createDevGateReport(edgeHealth) {
|
||||
"node scripts/m1-contract-smoke.mjs"
|
||||
],
|
||||
evidence: [
|
||||
"npm run check includes internal/cloud/server.test.mjs and validates /health plus /health/live."
|
||||
"npm run check includes internal/cloud/server-health.test.ts and validates /health plus /health/live."
|
||||
],
|
||||
summary: "Local cloud-api health checks pass and include service, commit, and image evidence fields."
|
||||
},
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
DEV_DB_ENV_CONTRACT,
|
||||
buildDbHealthContract,
|
||||
summarizeDbContract
|
||||
} from "../../internal/cloud/db-contract.mjs";
|
||||
} from "../../internal/cloud/db-contract.ts";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
codeAgentSecretRefPlaceholder
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
} from "../../internal/cloud/code-agent-contract.ts";
|
||||
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
|
||||
import { probeRegistryCapabilities } from "./registry-capabilities.mjs";
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
RUNTIME_DURABLE_ADAPTER_MISSING,
|
||||
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
|
||||
RUNTIME_STORE_KIND_POSTGRES
|
||||
} from "../../internal/db/runtime-store.mjs";
|
||||
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.mjs";
|
||||
} from "../../internal/db/runtime-store.ts";
|
||||
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.ts";
|
||||
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
@@ -7,8 +7,8 @@ const defaultKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const defaultNamespace = "hwlab-dev";
|
||||
const defaultDeployment = "hwlab-cloud-api";
|
||||
const defaultConfigMap = "hwlab-cloud-api-code-agent-hotfix";
|
||||
const defaultMountPath = "/app/internal/cloud/code-agent-chat.mjs";
|
||||
const defaultSubPath = "code-agent-chat.mjs";
|
||||
const defaultMountPath = "/app/internal/cloud/code-agent-chat.ts";
|
||||
const defaultSubPath = "code-agent-chat.ts";
|
||||
const defaultMarker = "runtime-hotfix-pc-gateway-shell";
|
||||
const defaultAnnotationKey = "hwlab.pikastech.local/pc-gateway-shell-hotfix";
|
||||
const defaultPodLabelSelector = "app.kubernetes.io/name=hwlab-cloud-api";
|
||||
|
||||
@@ -87,7 +87,7 @@ function fixtureResult(args, options = {}) {
|
||||
if (options.noHotfix) {
|
||||
return { ok: false, exitCode: 1, stdout: "", stderr: "Error from server (NotFound): configmaps \"hwlab-cloud-api-code-agent-hotfix\" not found" };
|
||||
}
|
||||
return okJson({ data: { "code-agent-chat.mjs": "// runtime-hotfix-pc-gateway-shell" } });
|
||||
return okJson({ data: { "code-agent-chat.ts": "// runtime-hotfix-pc-gateway-shell" } });
|
||||
}
|
||||
if (text.includes("get deployment")) {
|
||||
return okJson(deploymentFixture({ noHotfix: options.noHotfix }));
|
||||
@@ -127,7 +127,7 @@ function deploymentFixture({ noHotfix = false } = {}) {
|
||||
name: "code-agent-hotfix",
|
||||
configMap: {
|
||||
name: "hwlab-cloud-api-code-agent-hotfix",
|
||||
items: [{ key: "code-agent-chat.mjs", path: "code-agent-chat.mjs" }]
|
||||
items: [{ key: "code-agent-chat.ts", path: "code-agent-chat.ts" }]
|
||||
}
|
||||
}],
|
||||
containers: [{
|
||||
@@ -135,8 +135,8 @@ function deploymentFixture({ noHotfix = false } = {}) {
|
||||
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:dev",
|
||||
volumeMounts: noHotfix ? [] : [{
|
||||
name: "code-agent-hotfix",
|
||||
mountPath: "/app/internal/cloud/code-agent-chat.mjs",
|
||||
subPath: "code-agent-chat.mjs",
|
||||
mountPath: "/app/internal/cloud/code-agent-chat.ts",
|
||||
subPath: "code-agent-chat.ts",
|
||||
readOnly: true
|
||||
}]
|
||||
}]
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
|
||||
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS,
|
||||
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
|
||||
} from "../../internal/db/schema.mjs";
|
||||
} from "../../internal/db/schema.ts";
|
||||
import {
|
||||
PostgresCloudRuntimeStore,
|
||||
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
|
||||
buildPostgresPoolConfig
|
||||
} from "../../internal/db/runtime-store.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
} from "../../internal/db/runtime-store.ts";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
|
||||
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
|
||||
@@ -12,10 +12,11 @@ import {
|
||||
CLOUD_CORE_MIGRATION_ID,
|
||||
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
|
||||
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
|
||||
} from "../../internal/db/schema.mjs";
|
||||
} from "../../internal/db/schema.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
||||
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
|
||||
|
||||
test("source check is non-secret and does not attempt live DB access", async () => {
|
||||
const report = await buildDevRuntimeMigrationReport(parseArgs(["--check"]), {
|
||||
@@ -41,8 +42,8 @@ test("source check is non-secret and does not attempt live DB access", async ()
|
||||
|
||||
test("cloud-api image migration entrypoint exposes the same non-secret source check", async () => {
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
["cmd/hwlab-cloud-api/migrate.mjs", "--check"],
|
||||
bunCommand,
|
||||
["cmd/hwlab-cloud-api/migrate.ts", "--check"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
@@ -67,7 +68,7 @@ test("cloud-api image migration entrypoint exposes the same non-secret source ch
|
||||
test("cloud-api image migration entrypoint redacts invalid DSN-shaped argv failures", async () => {
|
||||
const secretArg = "postgresql://hwlab:secret-password@db.example.test:5432/hwlab?sslmode=require";
|
||||
await assert.rejects(
|
||||
execFileAsync(process.execPath, ["cmd/hwlab-cloud-api/migrate.mjs", secretArg], { cwd: repoRoot }),
|
||||
execFileAsync(bunCommand, ["cmd/hwlab-cloud-api/migrate.ts", secretArg], { cwd: repoRoot }),
|
||||
(error) => {
|
||||
const report = JSON.parse(error.stdout);
|
||||
assert.equal(report.conclusion.status, "blocked");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdir, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
|
||||
import {
|
||||
PostgresCloudRuntimeStore,
|
||||
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||||
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
|
||||
buildPostgresPoolConfig
|
||||
} from "../../internal/db/runtime-store.mjs";
|
||||
} from "../../internal/db/runtime-store.ts";
|
||||
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
||||
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
|
||||
|
||||
test("source check parses target role/database without leaking DB URL material", async () => {
|
||||
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
|
||||
@@ -33,8 +34,8 @@ test("source check parses target role/database without leaking DB URL material",
|
||||
|
||||
test("cloud-api image provisioning entrypoint exposes non-secret source check", async () => {
|
||||
const { stdout } = await execFileAsync(
|
||||
process.execPath,
|
||||
["cmd/hwlab-cloud-api/provision.mjs", "--check"],
|
||||
bunCommand,
|
||||
["cmd/hwlab-cloud-api/provision.ts", "--check"],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: {
|
||||
|
||||
@@ -353,6 +353,7 @@ function reuseCandidate(catalogRecord, affected) {
|
||||
}
|
||||
|
||||
function runtimeKindForService(serviceId) {
|
||||
if (serviceId === "hwlab-cloud-api") return "bun-command";
|
||||
if (serviceId === "hwlab-cloud-web") return "cloud-web";
|
||||
if (serviceId === "hwlab-cli") return "cli";
|
||||
if (serviceId === "hwlab-agent-skills") return "skills-bundle";
|
||||
@@ -360,6 +361,7 @@ function runtimeKindForService(serviceId) {
|
||||
}
|
||||
|
||||
function entrypointForService(serviceId) {
|
||||
if (serviceId === "hwlab-cloud-api") return "cmd/hwlab-cloud-api/main.ts";
|
||||
if (serviceId === "hwlab-cloud-web") return "web/hwlab-cloud-web/index.html";
|
||||
if (serviceId === "hwlab-cli") return "tools/hwlab-cli/bin/hwlab-cli.mjs";
|
||||
if (serviceId === "hwlab-agent-skills") return "skills/hwlab-agent-runtime/SKILL.md";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
M3_IO_CONTROL_CONTRACT_VERSION,
|
||||
M3_IO_CONTROL_ROUTE,
|
||||
describeM3IoControl
|
||||
} from "../../internal/cloud/m3-io-control.mjs";
|
||||
} from "../../internal/cloud/m3-io-control.ts";
|
||||
|
||||
export const M3_IO_E2E_CONTRACT_VERSION = "m3-io-control-e2e-v1";
|
||||
export const defaultFrontendUrl = "http://74.48.78.17:16666/";
|
||||
@@ -225,9 +225,9 @@ export async function buildM3IoControlSourceReport(options = {}) {
|
||||
|
||||
export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } = {}) {
|
||||
const [serverSource, jsonRpcSource, m3Source, appSource, htmlSource, artifactPublisherSource, cloudWebRouteSource] = await Promise.all([
|
||||
readRepoText(root, "internal/cloud/server.mjs"),
|
||||
readRepoText(root, "internal/cloud/json-rpc.mjs"),
|
||||
readRepoText(root, "internal/cloud/m3-io-control.mjs"),
|
||||
readRepoText(root, "internal/cloud/server.ts"),
|
||||
readRepoText(root, "internal/cloud/json-rpc.ts"),
|
||||
readRepoText(root, "internal/cloud/m3-io-control.ts"),
|
||||
readRepoText(root, "web/hwlab-cloud-web/app.mjs"),
|
||||
readRepoText(root, "web/hwlab-cloud-web/index.html"),
|
||||
readRepoText(root, "scripts/artifact-publish.mjs"),
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import {
|
||||
M3_IO_CHAIN,
|
||||
M3_IO_CONTROL_ROUTE
|
||||
} from "../../internal/cloud/m3-io-control.mjs";
|
||||
} from "../../internal/cloud/m3-io-control.ts";
|
||||
import {
|
||||
buildM3IoControlSourceReport,
|
||||
runM3IoControlLiveReport
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import assert from "node:assert/strict";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -6,11 +6,11 @@ import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
DEV_DB_ENV_CONTRACT,
|
||||
parseDbUrlContract
|
||||
} from "../internal/cloud/db-contract.mjs";
|
||||
} from "../internal/cloud/db-contract.ts";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
codeAgentSecretRefPlaceholder
|
||||
} from "../internal/cloud/code-agent-contract.mjs";
|
||||
} from "../internal/cloud/code-agent-contract.ts";
|
||||
import {
|
||||
HWLAB_M3_IO_API_BASE_URL_ENV,
|
||||
HWLAB_M3_IO_DEV_SERVICE_BASE_URL
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env bun
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../internal/cloud/db-contract.mjs";
|
||||
import { DEV_DB_ENV_CONTRACT } from "../internal/cloud/db-contract.ts";
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
@@ -215,12 +215,12 @@ function assertDurableRuntimeRunbook(value) {
|
||||
"Moved:",
|
||||
"Still blocked:",
|
||||
"No full M3, M4, or M5 acceptance is allowed while runtime durability is blocked",
|
||||
"node cmd/hwlab-cloud-api/provision.mjs --check",
|
||||
"node cmd/hwlab-cloud-api/provision.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"node cmd/hwlab-cloud-api/migrate.mjs --check",
|
||||
"node cmd/hwlab-cloud-api/migrate.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"bun cmd/hwlab-cloud-api/provision.ts --check",
|
||||
"bun cmd/hwlab-cloud-api/provision.ts --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"bun cmd/hwlab-cloud-api/provision.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
"bun cmd/hwlab-cloud-api/migrate.ts --check",
|
||||
"bun cmd/hwlab-cloud-api/migrate.ts --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"bun cmd/hwlab-cloud-api/migrate.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
|
||||
"DEV CD runtime migration Job uses",
|
||||
"node scripts/dev-runtime-provisioning.mjs --check",
|
||||
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
|
||||
@@ -247,11 +247,11 @@ function assertCloudApiImageRuntimeEntrypoints({ packageJson, artifactPublisher,
|
||||
assertIncludes(artifactPublisher, expected, `${label} artifact publisher`);
|
||||
}
|
||||
for (const expected of [
|
||||
"cmd/hwlab-cloud-api/provision.mjs",
|
||||
"cmd/hwlab-cloud-api/migrate.mjs"
|
||||
"cmd/hwlab-cloud-api/provision.ts",
|
||||
"cmd/hwlab-cloud-api/migrate.ts"
|
||||
]) {
|
||||
assertIncludes(packageJson.scripts?.check, `node --check ${expected}`, `${label} package check`);
|
||||
assertIncludes(packageJson.scripts?.check, `node ${expected} --check`, `${label} package runtime check`);
|
||||
assertIncludes(packageJson.scripts?.check, `bun build ${expected} --target=bun --packages=external`, `${label} package check`);
|
||||
assertIncludes(packageJson.scripts?.check, `bun ${expected} --check`, `${label} package runtime check`);
|
||||
}
|
||||
for (const expected of ["scripts/g14-artifact-publish.mjs", "runtime-dev", "runtime-prod"]) {
|
||||
assertIncludes(gitopsRenderSource, expected, `${label} G14 GitOps render source`);
|
||||
|
||||
Reference in New Issue
Block a user