Merge pull request #2667 from pikasTech/fix/2666-native-https
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success

修复 Native service 固定 HTTPS L1 入口
This commit is contained in:
Lyon
2026-07-18 19:07:32 +08:00
committed by GitHub
3 changed files with 25 additions and 5 deletions
+13 -2
View File
@@ -142,9 +142,11 @@ function serviceEnv(service: ServiceName, env: Record<string, string | undefined
}
function serviceEndpoints(service: ServiceName, env: Record<string, string | undefined>) {
const port = service === "api" ? requiredEnv(env, "CASERUN_API_PORT") : requiredEnv(env, "CASERUN_WEB_PORT");
const host = requiredEnv(env, "CASERUN_PUBLIC_HOST");
return { host, port: Number(port), url: `http://${host}:${Number(port)}${service === "api" ? "/health" : "/caserun"}` };
const publicBaseUrl = fixedHttpsOrigin(requiredEnv(env, "CASERUN_PUBLIC_BASE_URL"), "CASERUN_PUBLIC_BASE_URL");
validPort(port);
return { url: service === "api" ? publicBaseUrl : `${publicBaseUrl}/caserun` };
}
function validPort(value: string) { const port = Number(value); if (!Number.isInteger(port) || port < 1 || port > 65535) throw codedError("invalid_service_port", `invalid CaseRun service port: ${value}`); return port; }
function validService(value: string): ServiceName { if (value === "api" || value === "web") return value; throw codedError("invalid_service", "CaseRun service must be api or web"); }
async function readState(file: string) { return JSON.parse(await readFile(file, "utf8").catch(() => "null")); }
function alive(pid: number) { try { process.kill(pid, 0); return true; } catch { return false; } }
@@ -154,4 +156,13 @@ async function waitForExit(pid: number, timeoutMs: number) {
return !alive(pid);
}
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("native_exposure_config_required", `${key} is required from YAML-first native CaseRun config`); return value; }
function fixedHttpsOrigin(value: string, key: string) {
try {
const parsed = new URL(value);
if (parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash && !parsed.port) return parsed.origin;
} catch {
// Report one stable configuration error below.
}
throw codedError("native_exposure_config_invalid", `${key} must be a pathless HTTPS origin`);
}
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
+1 -1
View File
@@ -33,7 +33,7 @@ function help() {
],
transportContract: "--over-api only changes transport; --overapi is unsupported",
localConfig: ["WORKBENCH_MODE", "WORKBENCH_NATIVE_STATE_FILE"],
nativeServiceConfig: ["WORKBENCH_API_BIND_HOST", "WORKBENCH_API_PROBE_HOST", "WORKBENCH_API_PUBLIC_HOST", "WORKBENCH_API_PORT", "WORKBENCH_API_IDLE_TIMEOUT_SECONDS", "WORKBENCH_WORKER_BIND_HOST", "WORKBENCH_WORKER_PROBE_HOST", "WORKBENCH_WORKER_HEALTH_PORT", "WORKBENCH_WEB_BIND_HOST", "WORKBENCH_WEB_PROBE_HOST", "WORKBENCH_WEB_PUBLIC_HOST", "WORKBENCH_WEB_PORT", "WORKBENCH_NATIVE_API_URL", "WORKBENCH_WEB_RUNTIME_CONFIG"],
nativeServiceConfig: ["WORKBENCH_PUBLIC_BASE_URL", "WORKBENCH_API_BIND_HOST", "WORKBENCH_API_PROBE_HOST", "WORKBENCH_API_PORT", "WORKBENCH_API_IDLE_TIMEOUT_SECONDS", "WORKBENCH_WORKER_BIND_HOST", "WORKBENCH_WORKER_PROBE_HOST", "WORKBENCH_WORKER_HEALTH_PORT", "WORKBENCH_WEB_BIND_HOST", "WORKBENCH_WEB_PROBE_HOST", "WORKBENCH_WEB_PORT", "WORKBENCH_NATIVE_API_URL", "WORKBENCH_WEB_RUNTIME_CONFIG"],
apiConfig: ["WORKBENCH_API_URL", "HWLAB_API_KEY"]
};
}
+11 -2
View File
@@ -87,18 +87,27 @@ function serviceEndpoints(service: ServiceName, env: Record<string, string | und
const prefix = service === "api" ? "WORKBENCH_API" : "WORKBENCH_WEB";
const bindHost = requiredEnv(env, `${prefix}_BIND_HOST`);
const probeHost = requiredEnv(env, `${prefix}_PROBE_HOST`);
const publicHost = requiredEnv(env, `${prefix}_PUBLIC_HOST`);
const publicBaseUrl = fixedHttpsOrigin(requiredEnv(env, "WORKBENCH_PUBLIC_BASE_URL"), "WORKBENCH_PUBLIC_BASE_URL");
const port = requiredEnv(env, `${prefix}_PORT`);
const pathname = service === "api" ? "/health/ready" : "/workbench";
return {
bind: { host: bindHost, port: validPort(port) },
probe: endpoint(probeHost, port, pathname),
public: endpoint(publicHost, port, service === "api" ? "" : pathname)
public: { url: service === "api" ? publicBaseUrl : `${publicBaseUrl}${pathname}` }
};
}
function endpoint(host: string, port: string, pathname: string) { const numericPort = validPort(port); return { host, port: numericPort, url: `http://${host}:${numericPort}${pathname}` }; }
function validPort(value: string) { const port = Number(value); if (!Number.isInteger(port) || port < 1 || port > 65535) throw codedError("invalid_service_port", `invalid Workbench service port: ${value}`); return port; }
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("native_exposure_config_required", `${key} is required from the owning YAML native development exposure`); return value; }
function fixedHttpsOrigin(value: string, key: string) {
try {
const parsed = new URL(value);
if (parsed.protocol === "https:" && parsed.pathname === "/" && !parsed.search && !parsed.hash && !parsed.port) return parsed.origin;
} catch {
// Report one stable configuration error below.
}
throw codedError("native_exposure_config_invalid", `${key} must be a pathless HTTPS origin`);
}
async function probeHealth(url: string) {
try { const response = await fetch(url, { signal: AbortSignal.timeout(1000) }); return { ok: response.ok, status: response.status, url }; }
catch (error) { return { ok: false, status: null, url, error: error instanceof Error ? error.message : String(error) }; }