|
|
@@ -2,6 +2,8 @@
|
|
|
|
// Responsibility: YAML-first D518/D601 HWLAB source workspace status and bootstrap for web-probe clients.
|
|
|
|
// Responsibility: YAML-first D518/D601 HWLAB source workspace status and bootstrap for web-probe clients.
|
|
|
|
import { createHash } from "node:crypto";
|
|
|
|
import { createHash } from "node:crypto";
|
|
|
|
import { posix as posixPath } from "node:path";
|
|
|
|
import { posix as posixPath } from "node:path";
|
|
|
|
|
|
|
|
import type { UniDeskConfig } from "../config";
|
|
|
|
|
|
|
|
import { ensureGithubSshIdentityForProvider } from "../deploy-ssh-identity";
|
|
|
|
import { hwlabNodeControlPlaneSourceWorkspaceBootstrap } from "../hwlab-node-control-plane";
|
|
|
|
import { hwlabNodeControlPlaneSourceWorkspaceBootstrap } from "../hwlab-node-control-plane";
|
|
|
|
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec, type HwlabRuntimeSourceWorkspaceHostDependenciesSpec, type HwlabRuntimeSourceWorkspaceSpec } from "../hwlab-node-lanes";
|
|
|
|
import { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec, type HwlabRuntimeSourceWorkspaceHostDependenciesSpec, type HwlabRuntimeSourceWorkspaceSpec } from "../hwlab-node-lanes";
|
|
|
|
import { parseNodeScopedDelegatedOptions } from "./plan";
|
|
|
|
import { parseNodeScopedDelegatedOptions } from "./plan";
|
|
|
@@ -10,10 +12,10 @@ import { compactRuntimeCommand, parseLastJsonLineObject } from "./runtime-common
|
|
|
|
import { commaListField, keyValueLinesFromText, numericField, shellQuote, statusText } from "./utils";
|
|
|
|
import { commaListField, keyValueLinesFromText, numericField, shellQuote, statusText } from "./utils";
|
|
|
|
|
|
|
|
|
|
|
|
type ScopedNodeOptions = ReturnType<typeof parseNodeScopedDelegatedOptions>;
|
|
|
|
type ScopedNodeOptions = ReturnType<typeof parseNodeScopedDelegatedOptions>;
|
|
|
|
type SourceWorkspaceAction = "status" | "bootstrap" | "host-deps";
|
|
|
|
type SourceWorkspaceAction = "status" | "sync" | "bootstrap" | "host-deps";
|
|
|
|
type SourceWorkspaceHostDependenciesAction = "status" | "sync";
|
|
|
|
type SourceWorkspaceHostDependenciesAction = "status" | "sync";
|
|
|
|
|
|
|
|
|
|
|
|
export function nodeRuntimeSourceWorkspaceCommand(scoped: ScopedNodeOptions): Record<string, unknown> {
|
|
|
|
export async function nodeRuntimeSourceWorkspaceCommand(config: UniDeskConfig, scoped: ScopedNodeOptions): Promise<Record<string, unknown>> {
|
|
|
|
const action = sourceWorkspaceAction(scoped);
|
|
|
|
const action = sourceWorkspaceAction(scoped);
|
|
|
|
const sourceWorkspace = scoped.spec.sourceWorkspace;
|
|
|
|
const sourceWorkspace = scoped.spec.sourceWorkspace;
|
|
|
|
if (sourceWorkspace === undefined) {
|
|
|
|
if (sourceWorkspace === undefined) {
|
|
|
@@ -29,18 +31,22 @@ export function nodeRuntimeSourceWorkspaceCommand(scoped: ScopedNodeOptions): Re
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (action === "host-deps") return nodeRuntimeSourceWorkspaceHostDependencies(scoped, sourceWorkspace);
|
|
|
|
if (action === "host-deps") return nodeRuntimeSourceWorkspaceHostDependencies(scoped, sourceWorkspace);
|
|
|
|
|
|
|
|
if (action === "sync") return await nodeRuntimeSourceWorkspaceSync(config, scoped, sourceWorkspace);
|
|
|
|
if (action === "status") return nodeRuntimeSourceWorkspaceStatus(scoped, sourceWorkspace);
|
|
|
|
if (action === "status") return nodeRuntimeSourceWorkspaceStatus(scoped, sourceWorkspace);
|
|
|
|
return nodeRuntimeSourceWorkspaceBootstrap(scoped, sourceWorkspace);
|
|
|
|
return nodeRuntimeSourceWorkspaceBootstrap(scoped, sourceWorkspace);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sourceWorkspaceAction(scoped: ScopedNodeOptions): SourceWorkspaceAction {
|
|
|
|
function sourceWorkspaceAction(scoped: ScopedNodeOptions): SourceWorkspaceAction {
|
|
|
|
const value = scoped.originalArgs[1];
|
|
|
|
const value = scoped.originalArgs[1];
|
|
|
|
if (value !== "status" && value !== "bootstrap" && value !== "host-deps") {
|
|
|
|
if (value !== "status" && value !== "sync" && value !== "bootstrap" && value !== "host-deps") {
|
|
|
|
throw new Error("control-plane source-workspace usage: source-workspace status|bootstrap|host-deps --node NODE --lane vNN [--dry-run|--confirm]");
|
|
|
|
throw new Error("control-plane source-workspace usage: source-workspace status|sync|bootstrap|host-deps --node NODE --lane vNN [--dry-run|--confirm]");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (value === "status" && (scoped.confirm || scoped.dryRun)) {
|
|
|
|
if (value === "status" && (scoped.confirm || scoped.dryRun)) {
|
|
|
|
throw new Error("control-plane source-workspace status is read-only and does not accept --dry-run or --confirm");
|
|
|
|
throw new Error("control-plane source-workspace status is read-only and does not accept --dry-run or --confirm");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value === "sync" && scoped.wait) {
|
|
|
|
|
|
|
|
throw new Error("control-plane source-workspace sync is synchronous and does not accept --wait");
|
|
|
|
|
|
|
|
}
|
|
|
|
return value;
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -116,6 +122,304 @@ function nodeRuntimeSourceWorkspaceBootstrap(scoped: ScopedNodeOptions, sourceWo
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function nodeRuntimeSourceWorkspaceSync(config: UniDeskConfig, scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Promise<Record<string, unknown>> {
|
|
|
|
|
|
|
|
if (sourceWorkspace.git === undefined) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: false,
|
|
|
|
|
|
|
|
command: `hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane}`,
|
|
|
|
|
|
|
|
node: scoped.node,
|
|
|
|
|
|
|
|
lane: scoped.lane,
|
|
|
|
|
|
|
|
mode: "yaml-missing",
|
|
|
|
|
|
|
|
mutation: false,
|
|
|
|
|
|
|
|
configPath: hwlabRuntimeLaneConfigPath(),
|
|
|
|
|
|
|
|
degradedReason: "source-workspace-git-yaml-missing",
|
|
|
|
|
|
|
|
message: "sourceWorkspace.git must declare remoteName, remoteUrl, identityTarget/identityId, proxyEnvPath and up-to-date policy before sync can run.",
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const dryRun = scoped.dryRun || !scoped.confirm;
|
|
|
|
|
|
|
|
const before = nodeRuntimeSourceWorkspaceGitStatus(scoped, sourceWorkspace);
|
|
|
|
|
|
|
|
const identityPlan = sourceWorkspaceIdentityPlan(scoped, sourceWorkspace);
|
|
|
|
|
|
|
|
if (dryRun) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
|
|
command: `hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane}`,
|
|
|
|
|
|
|
|
node: scoped.node,
|
|
|
|
|
|
|
|
lane: scoped.lane,
|
|
|
|
|
|
|
|
mode: "dry-run",
|
|
|
|
|
|
|
|
mutation: false,
|
|
|
|
|
|
|
|
configPath: hwlabRuntimeLaneConfigPath(),
|
|
|
|
|
|
|
|
expected: sourceWorkspaceExpected(scoped.spec, sourceWorkspace),
|
|
|
|
|
|
|
|
before,
|
|
|
|
|
|
|
|
identity: identityPlan,
|
|
|
|
|
|
|
|
sync: {
|
|
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
|
|
status: "dry-run",
|
|
|
|
|
|
|
|
remoteName: sourceWorkspace.git.remoteName,
|
|
|
|
|
|
|
|
remoteUrl: sourceWorkspace.git.remoteUrl,
|
|
|
|
|
|
|
|
sourceBranch: scoped.spec.sourceBranch,
|
|
|
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
after: null,
|
|
|
|
|
|
|
|
next: { confirm: `bun scripts/cli.ts hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane} --confirm` },
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const identity = await sourceWorkspaceEnsureIdentity(config, scoped, sourceWorkspace);
|
|
|
|
|
|
|
|
if (identity.ok === false) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: false,
|
|
|
|
|
|
|
|
command: `hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane}`,
|
|
|
|
|
|
|
|
node: scoped.node,
|
|
|
|
|
|
|
|
lane: scoped.lane,
|
|
|
|
|
|
|
|
mode: "identity-distribution-failed",
|
|
|
|
|
|
|
|
mutation: false,
|
|
|
|
|
|
|
|
configPath: hwlabRuntimeLaneConfigPath(),
|
|
|
|
|
|
|
|
expected: sourceWorkspaceExpected(scoped.spec, sourceWorkspace),
|
|
|
|
|
|
|
|
before,
|
|
|
|
|
|
|
|
identity,
|
|
|
|
|
|
|
|
sync: null,
|
|
|
|
|
|
|
|
after: null,
|
|
|
|
|
|
|
|
degradedReason: "source-workspace-identity-distribution-failed",
|
|
|
|
|
|
|
|
next: { retry: `bun scripts/cli.ts hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane} --confirm` },
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const result = runTransHostScript(scoped.node, sourceWorkspaceSyncScript(scoped.spec, sourceWorkspace), "", scoped.timeoutSeconds);
|
|
|
|
|
|
|
|
const payload = parseLastJsonLineObject(statusText(result));
|
|
|
|
|
|
|
|
const after = nodeRuntimeSourceWorkspaceGitStatus(scoped, sourceWorkspace);
|
|
|
|
|
|
|
|
const ok = result.exitCode === 0 && payload.ok === true && after.ok === true;
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok,
|
|
|
|
|
|
|
|
command: `hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane}`,
|
|
|
|
|
|
|
|
node: scoped.node,
|
|
|
|
|
|
|
|
lane: scoped.lane,
|
|
|
|
|
|
|
|
mode: "confirmed-sync",
|
|
|
|
|
|
|
|
mutation: true,
|
|
|
|
|
|
|
|
configPath: hwlabRuntimeLaneConfigPath(),
|
|
|
|
|
|
|
|
expected: sourceWorkspaceExpected(scoped.spec, sourceWorkspace),
|
|
|
|
|
|
|
|
before,
|
|
|
|
|
|
|
|
identity,
|
|
|
|
|
|
|
|
sync: payload,
|
|
|
|
|
|
|
|
result: compactRuntimeCommand(result),
|
|
|
|
|
|
|
|
after,
|
|
|
|
|
|
|
|
degradedReason: ok ? undefined : "source-workspace-sync-failed",
|
|
|
|
|
|
|
|
next: ok
|
|
|
|
|
|
|
|
? { status: `bun scripts/cli.ts hwlab nodes control-plane source-workspace status --node ${scoped.node} --lane ${scoped.lane}` }
|
|
|
|
|
|
|
|
: { retry: `bun scripts/cli.ts hwlab nodes control-plane source-workspace sync --node ${scoped.node} --lane ${scoped.lane} --confirm` },
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sourceWorkspaceIdentityPlan(scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Record<string, unknown> {
|
|
|
|
|
|
|
|
const git = sourceWorkspace.git;
|
|
|
|
|
|
|
|
if (git === undefined) return { required: false, valuesPrinted: false };
|
|
|
|
|
|
|
|
const remoteNeedsSsh = remoteUrlNeedsSshIdentity(git.remoteUrl);
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
required: remoteNeedsSsh,
|
|
|
|
|
|
|
|
source: "sourceWorkspace.git",
|
|
|
|
|
|
|
|
identityTarget: git.identityTarget ?? null,
|
|
|
|
|
|
|
|
identityId: git.identityId ?? null,
|
|
|
|
|
|
|
|
configRefs: {
|
|
|
|
|
|
|
|
lane: `${hwlabRuntimeLaneConfigPath()}#lanes.${scoped.lane}.targets.${scoped.node}.sourceWorkspace.git`,
|
|
|
|
|
|
|
|
identity: git.identityTarget === undefined || git.identityId === undefined ? null : `config/deploy-ssh-identities.yaml#targets.${git.identityTarget}.identities[${git.identityId}]`,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function sourceWorkspaceEnsureIdentity(config: UniDeskConfig, scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Promise<Record<string, unknown>> {
|
|
|
|
|
|
|
|
const git = sourceWorkspace.git;
|
|
|
|
|
|
|
|
if (git === undefined || !remoteUrlNeedsSshIdentity(git.remoteUrl)) {
|
|
|
|
|
|
|
|
return { ok: true, required: false, status: "not-required", valuesPrinted: false };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (git.identityTarget === undefined || git.identityId === undefined) {
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: false,
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
status: "yaml-missing",
|
|
|
|
|
|
|
|
degradedReason: "source-workspace-git-identity-yaml-missing",
|
|
|
|
|
|
|
|
message: `sourceWorkspace.git for node=${scoped.node} lane=${scoped.lane} uses SSH remoteUrl and must declare identityTarget and identityId.`,
|
|
|
|
|
|
|
|
configPath: hwlabRuntimeLaneConfigPath(),
|
|
|
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await ensureGithubSshIdentityForProvider(config, git.identityTarget, git.identityId);
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: result.ok,
|
|
|
|
|
|
|
|
required: true,
|
|
|
|
|
|
|
|
status: result.ok ? "distributed" : "failed",
|
|
|
|
|
|
|
|
identityTarget: result.targetId ?? git.identityTarget,
|
|
|
|
|
|
|
|
identityId: result.identityId ?? git.identityId,
|
|
|
|
|
|
|
|
fingerprint: result.fingerprint,
|
|
|
|
|
|
|
|
seededFromLocal: result.seededFromLocal,
|
|
|
|
|
|
|
|
detail: result.ok ? result.detail : "identity distribution failed; inspect the returned redacted raw tail for details",
|
|
|
|
|
|
|
|
configTruth: result.configTruth,
|
|
|
|
|
|
|
|
raw: result.ok ? null : result.raw,
|
|
|
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function remoteUrlNeedsSshIdentity(remoteUrl: string): boolean {
|
|
|
|
|
|
|
|
return remoteUrl.startsWith("git@") || remoteUrl.startsWith("ssh://");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function nodeRuntimeSourceWorkspaceGitStatus(scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Record<string, unknown> {
|
|
|
|
|
|
|
|
if (sourceWorkspace.git === undefined) {
|
|
|
|
|
|
|
|
return { ok: false, status: "yaml-missing", degradedReason: "source-workspace-git-yaml-missing", valuesPrinted: false };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = runTransHostScript(scoped.node, sourceWorkspaceGitStatusScript(scoped.spec, sourceWorkspace), "", scoped.timeoutSeconds);
|
|
|
|
|
|
|
|
const payload = parseLastJsonLineObject(statusText(result));
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
ok: result.exitCode === 0 && payload.ok === true,
|
|
|
|
|
|
|
|
...payload,
|
|
|
|
|
|
|
|
probe: compactRuntimeCommand(result),
|
|
|
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sourceWorkspaceGitStatusScript(spec: HwlabRuntimeLaneSpec, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): string {
|
|
|
|
|
|
|
|
const git = sourceWorkspace.git;
|
|
|
|
|
|
|
|
if (git === undefined) throw new Error("sourceWorkspace.git is required");
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
"set +e",
|
|
|
|
|
|
|
|
`workspace=${shellQuote(spec.workspace)}`,
|
|
|
|
|
|
|
|
`expected_branch=${shellQuote(spec.sourceBranch)}`,
|
|
|
|
|
|
|
|
`remote_name=${shellQuote(git.remoteName)}`,
|
|
|
|
|
|
|
|
`expected_remote_url=${shellQuote(git.remoteUrl)}`,
|
|
|
|
|
|
|
|
`verify_remote=${shellQuote(git.verifyRemote ? "yes" : "no")}`,
|
|
|
|
|
|
|
|
`require_up_to_date=${shellQuote(git.requireUpToDate ? "yes" : "no")}`,
|
|
|
|
|
|
|
|
`proxy_env_path=${shellQuote(git.proxyEnvPath ?? "")}`,
|
|
|
|
|
|
|
|
`git_timeout=${String(spec.downloadProfile.git.timeoutSeconds)}`,
|
|
|
|
|
|
|
|
`http_proxy_value=${shellQuote(spec.networkProfile.proxy.http)}`,
|
|
|
|
|
|
|
|
`https_proxy_value=${shellQuote(spec.networkProfile.proxy.https)}`,
|
|
|
|
|
|
|
|
`all_proxy_value=${shellQuote(spec.networkProfile.proxy.all)}`,
|
|
|
|
|
|
|
|
`no_proxy_value=${shellQuote(spec.networkProfile.proxy.noProxy.join(","))}`,
|
|
|
|
|
|
|
|
"tmp_dir=$(mktemp -d)",
|
|
|
|
|
|
|
|
"trap 'rm -rf \"$tmp_dir\"' EXIT",
|
|
|
|
|
|
|
|
"export GIT_TERMINAL_PROMPT=0",
|
|
|
|
|
|
|
|
"workspace_exists=false",
|
|
|
|
|
|
|
|
"git_dir_exists=false",
|
|
|
|
|
|
|
|
"workspace_clean=false",
|
|
|
|
|
|
|
|
"branch=",
|
|
|
|
|
|
|
|
"local_head=",
|
|
|
|
|
|
|
|
"remote_url=",
|
|
|
|
|
|
|
|
"remote_matches=false",
|
|
|
|
|
|
|
|
"remote_reachable=null",
|
|
|
|
|
|
|
|
"remote_head=",
|
|
|
|
|
|
|
|
"remote_probe_exit=",
|
|
|
|
|
|
|
|
"remote_probe_error_tail=",
|
|
|
|
|
|
|
|
"status_short=",
|
|
|
|
|
|
|
|
"failure_kind=",
|
|
|
|
|
|
|
|
"if [ ! -d \"$workspace\" ]; then",
|
|
|
|
|
|
|
|
" failure_kind=workspace-missing",
|
|
|
|
|
|
|
|
"else",
|
|
|
|
|
|
|
|
" workspace_exists=true",
|
|
|
|
|
|
|
|
" if ! git -C \"$workspace\" rev-parse --git-dir >/dev/null 2>&1; then",
|
|
|
|
|
|
|
|
" failure_kind=workspace-git-missing",
|
|
|
|
|
|
|
|
" else",
|
|
|
|
|
|
|
|
" git_dir_exists=true",
|
|
|
|
|
|
|
|
" branch=$(git -C \"$workspace\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" local_head=$(git -C \"$workspace\" rev-parse HEAD 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" remote_url=$(git -C \"$workspace\" remote get-url \"$remote_name\" 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" status_short=$(git -C \"$workspace\" status --short 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" [ -z \"$status_short\" ] && workspace_clean=true",
|
|
|
|
|
|
|
|
" [ \"$remote_url\" = \"$expected_remote_url\" ] && remote_matches=true",
|
|
|
|
|
|
|
|
" if [ \"$verify_remote\" = yes ]; then",
|
|
|
|
|
|
|
|
" if [ -n \"$proxy_env_path\" ] && [ -f \"$proxy_env_path\" ]; then . \"$proxy_env_path\" 2>/dev/null || true; fi",
|
|
|
|
|
|
|
|
" export HTTP_PROXY=\"${HTTP_PROXY:-$http_proxy_value}\" HTTPS_PROXY=\"${HTTPS_PROXY:-$https_proxy_value}\" ALL_PROXY=\"${ALL_PROXY:-$all_proxy_value}\" NO_PROXY=\"${NO_PROXY:-$no_proxy_value}\"",
|
|
|
|
|
|
|
|
" export http_proxy=\"${http_proxy:-$HTTP_PROXY}\" https_proxy=\"${https_proxy:-$HTTPS_PROXY}\" all_proxy=\"${all_proxy:-$ALL_PROXY}\" no_proxy=\"${no_proxy:-$NO_PROXY}\"",
|
|
|
|
|
|
|
|
" timeout \"$git_timeout\" git -C \"$workspace\" ls-remote \"$expected_remote_url\" \"refs/heads/$expected_branch\" >\"$tmp_dir/remote.out\" 2>\"$tmp_dir/remote.err\"",
|
|
|
|
|
|
|
|
" remote_probe_exit=$?",
|
|
|
|
|
|
|
|
" remote_head=$(awk '{print $1}' \"$tmp_dir/remote.out\" | head -n 1)",
|
|
|
|
|
|
|
|
" remote_probe_error_tail=$(tail -c 1000 \"$tmp_dir/remote.err\" 2>/dev/null | tr '\\n\\t' ' ' | cut -c1-1000)",
|
|
|
|
|
|
|
|
" if [ \"$remote_probe_exit\" = 0 ] && [ -n \"$remote_head\" ]; then remote_reachable=true; else remote_reachable=false; fi",
|
|
|
|
|
|
|
|
" fi",
|
|
|
|
|
|
|
|
" fi",
|
|
|
|
|
|
|
|
"fi",
|
|
|
|
|
|
|
|
"WORKSPACE=\"$workspace\" EXPECTED_BRANCH=\"$expected_branch\" WORKSPACE_EXISTS=\"$workspace_exists\" GIT_DIR_EXISTS=\"$git_dir_exists\" WORKSPACE_CLEAN=\"$workspace_clean\" BRANCH=\"$branch\" LOCAL_HEAD=\"$local_head\" REMOTE_NAME=\"$remote_name\" EXPECTED_REMOTE_URL=\"$expected_remote_url\" REMOTE_URL=\"$remote_url\" REMOTE_MATCHES=\"$remote_matches\" VERIFY_REMOTE=\"$verify_remote\" REQUIRE_UP_TO_DATE=\"$require_up_to_date\" REMOTE_REACHABLE=\"$remote_reachable\" REMOTE_HEAD=\"$remote_head\" REMOTE_PROBE_EXIT=\"$remote_probe_exit\" REMOTE_PROBE_ERROR_TAIL=\"$remote_probe_error_tail\" STATUS_SHORT=\"$status_short\" FAILURE_KIND=\"$failure_kind\" node <<'NODE'",
|
|
|
|
|
|
|
|
"const statusShort = process.env.STATUS_SHORT || '';",
|
|
|
|
|
|
|
|
"const remoteHead = process.env.REMOTE_HEAD || '';",
|
|
|
|
|
|
|
|
"const remoteRequired = process.env.VERIFY_REMOTE === 'yes';",
|
|
|
|
|
|
|
|
"const upToDateRequired = process.env.REQUIRE_UP_TO_DATE === 'yes';",
|
|
|
|
|
|
|
|
"const remoteReachable = process.env.REMOTE_REACHABLE === 'null' ? null : process.env.REMOTE_REACHABLE === 'true';",
|
|
|
|
|
|
|
|
"const localHead = process.env.LOCAL_HEAD || '';",
|
|
|
|
|
|
|
|
"const ok = process.env.WORKSPACE_EXISTS === 'true' && process.env.GIT_DIR_EXISTS === 'true' && process.env.WORKSPACE_CLEAN === 'true' && process.env.BRANCH === process.env.EXPECTED_BRANCH && process.env.REMOTE_MATCHES === 'true' && (!remoteRequired || remoteReachable === true) && (!upToDateRequired || !remoteHead || localHead === remoteHead);",
|
|
|
|
|
|
|
|
"console.log(JSON.stringify({ ok, status: ok ? 'ready' : 'not-ready', workspace: process.env.WORKSPACE, expectedBranch: process.env.EXPECTED_BRANCH, workspaceExists: process.env.WORKSPACE_EXISTS === 'true', gitDirExists: process.env.GIT_DIR_EXISTS === 'true', workspaceClean: process.env.WORKSPACE_CLEAN === 'true', branch: process.env.BRANCH || null, localHead: localHead || null, remoteName: process.env.REMOTE_NAME, expectedRemoteUrl: process.env.EXPECTED_REMOTE_URL, remoteUrl: process.env.REMOTE_URL || null, remoteMatchesYaml: process.env.REMOTE_MATCHES === 'true', remoteReachabilityRequired: remoteRequired, remoteReachable, remoteHead: remoteHead || null, remoteProbeExitCode: process.env.REMOTE_PROBE_EXIT ? Number(process.env.REMOTE_PROBE_EXIT) : null, remoteProbeErrorTail: process.env.REMOTE_PROBE_ERROR_TAIL || null, remoteUpToDateRequired: upToDateRequired, remoteUpToDate: remoteHead ? localHead === remoteHead : null, statusShortLines: statusShort ? statusShort.split(/\\n/).filter(Boolean).length : 0, statusShortPreview: statusShort.replace(/[\\n\\t]+/g, ' ').slice(0, 1000) || null, failureKind: process.env.FAILURE_KIND || null, valuesPrinted: false }));",
|
|
|
|
|
|
|
|
"NODE",
|
|
|
|
|
|
|
|
].join("\n");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sourceWorkspaceSyncScript(spec: HwlabRuntimeLaneSpec, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): string {
|
|
|
|
|
|
|
|
const git = sourceWorkspace.git;
|
|
|
|
|
|
|
|
if (git === undefined) throw new Error("sourceWorkspace.git is required");
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
|
|
"set +e",
|
|
|
|
|
|
|
|
`workspace=${shellQuote(spec.workspace)}`,
|
|
|
|
|
|
|
|
`expected_branch=${shellQuote(spec.sourceBranch)}`,
|
|
|
|
|
|
|
|
`remote_name=${shellQuote(git.remoteName)}`,
|
|
|
|
|
|
|
|
`remote_url=${shellQuote(git.remoteUrl)}`,
|
|
|
|
|
|
|
|
`proxy_env_path=${shellQuote(git.proxyEnvPath ?? "")}`,
|
|
|
|
|
|
|
|
`git_timeout=${String(spec.downloadProfile.git.timeoutSeconds)}`,
|
|
|
|
|
|
|
|
`http_proxy_value=${shellQuote(spec.networkProfile.proxy.http)}`,
|
|
|
|
|
|
|
|
`https_proxy_value=${shellQuote(spec.networkProfile.proxy.https)}`,
|
|
|
|
|
|
|
|
`all_proxy_value=${shellQuote(spec.networkProfile.proxy.all)}`,
|
|
|
|
|
|
|
|
`no_proxy_value=${shellQuote(spec.networkProfile.proxy.noProxy.join(","))}`,
|
|
|
|
|
|
|
|
"tmp_dir=$(mktemp -d)",
|
|
|
|
|
|
|
|
"trap 'rm -rf \"$tmp_dir\"' EXIT",
|
|
|
|
|
|
|
|
"export GIT_TERMINAL_PROMPT=0",
|
|
|
|
|
|
|
|
"if [ -n \"$proxy_env_path\" ] && [ -f \"$proxy_env_path\" ]; then . \"$proxy_env_path\" 2>/dev/null || true; fi",
|
|
|
|
|
|
|
|
"export HTTP_PROXY=\"${HTTP_PROXY:-$http_proxy_value}\" HTTPS_PROXY=\"${HTTPS_PROXY:-$https_proxy_value}\" ALL_PROXY=\"${ALL_PROXY:-$all_proxy_value}\" NO_PROXY=\"${NO_PROXY:-$no_proxy_value}\"",
|
|
|
|
|
|
|
|
"export http_proxy=\"${http_proxy:-$HTTP_PROXY}\" https_proxy=\"${https_proxy:-$HTTPS_PROXY}\" all_proxy=\"${all_proxy:-$ALL_PROXY}\" no_proxy=\"${no_proxy:-$NO_PROXY}\"",
|
|
|
|
|
|
|
|
"failure_kind=",
|
|
|
|
|
|
|
|
"fetch_exit=1",
|
|
|
|
|
|
|
|
"update_exit=1",
|
|
|
|
|
|
|
|
"remote_url_after=",
|
|
|
|
|
|
|
|
"remote_head=",
|
|
|
|
|
|
|
|
"local_head=",
|
|
|
|
|
|
|
|
"branch=",
|
|
|
|
|
|
|
|
"status_short=",
|
|
|
|
|
|
|
|
"stderr_tail=",
|
|
|
|
|
|
|
|
"if [ ! -d \"$workspace/.git\" ]; then",
|
|
|
|
|
|
|
|
" failure_kind=workspace-git-missing",
|
|
|
|
|
|
|
|
"elif ! cd \"$workspace\"; then",
|
|
|
|
|
|
|
|
" failure_kind=workspace-cd-failed",
|
|
|
|
|
|
|
|
"else",
|
|
|
|
|
|
|
|
" status_short=$(git status --short 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" if [ -n \"$status_short\" ]; then",
|
|
|
|
|
|
|
|
" failure_kind=workspace-dirty",
|
|
|
|
|
|
|
|
" elif [ \"$branch\" != \"$expected_branch\" ]; then",
|
|
|
|
|
|
|
|
" failure_kind=branch-mismatch",
|
|
|
|
|
|
|
|
" else",
|
|
|
|
|
|
|
|
" git remote set-url \"$remote_name\" \"$remote_url\" >\"$tmp_dir/remote.out\" 2>\"$tmp_dir/remote.err\" || git remote add \"$remote_name\" \"$remote_url\" >>\"$tmp_dir/remote.out\" 2>>\"$tmp_dir/remote.err\"",
|
|
|
|
|
|
|
|
" remote_set_exit=$?",
|
|
|
|
|
|
|
|
" remote_url_after=$(git remote get-url \"$remote_name\" 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" if [ \"$remote_set_exit\" -ne 0 ]; then",
|
|
|
|
|
|
|
|
" failure_kind=remote-config-failed",
|
|
|
|
|
|
|
|
" else",
|
|
|
|
|
|
|
|
" timeout \"$git_timeout\" git fetch \"$remote_name\" \"$expected_branch:refs/remotes/$remote_name/$expected_branch\" >\"$tmp_dir/fetch.out\" 2>\"$tmp_dir/fetch.err\"",
|
|
|
|
|
|
|
|
" fetch_exit=$?",
|
|
|
|
|
|
|
|
" if [ \"$fetch_exit\" -ne 0 ]; then",
|
|
|
|
|
|
|
|
" failure_kind=git-fetch-failed",
|
|
|
|
|
|
|
|
" else",
|
|
|
|
|
|
|
|
" remote_head=$(git rev-parse \"refs/remotes/$remote_name/$expected_branch\" 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" git merge --ff-only \"refs/remotes/$remote_name/$expected_branch\" >\"$tmp_dir/update.out\" 2>\"$tmp_dir/update.err\"",
|
|
|
|
|
|
|
|
" update_exit=$?",
|
|
|
|
|
|
|
|
" if [ \"$update_exit\" -ne 0 ]; then failure_kind=git-ff-only-failed; fi",
|
|
|
|
|
|
|
|
" fi",
|
|
|
|
|
|
|
|
" fi",
|
|
|
|
|
|
|
|
" fi",
|
|
|
|
|
|
|
|
" local_head=$(git rev-parse HEAD 2>/dev/null || true)",
|
|
|
|
|
|
|
|
" status_short=$(git status --short 2>/dev/null || true)",
|
|
|
|
|
|
|
|
"fi",
|
|
|
|
|
|
|
|
"stderr_tail=$(cat \"$tmp_dir\"/*.err 2>/dev/null | tail -c 2000 | tr '\\n\\t' ' ' | cut -c1-2000)",
|
|
|
|
|
|
|
|
"WORKSPACE=\"$workspace\" EXPECTED_BRANCH=\"$expected_branch\" REMOTE_NAME=\"$remote_name\" REMOTE_URL=\"$remote_url\" REMOTE_URL_AFTER=\"$remote_url_after\" BRANCH=\"$branch\" LOCAL_HEAD=\"$local_head\" REMOTE_HEAD=\"$remote_head\" FETCH_EXIT=\"$fetch_exit\" UPDATE_EXIT=\"$update_exit\" STATUS_SHORT=\"$status_short\" FAILURE_KIND=\"$failure_kind\" STDERR_TAIL=\"$stderr_tail\" node <<'NODE'",
|
|
|
|
|
|
|
|
"const statusShort = process.env.STATUS_SHORT || '';",
|
|
|
|
|
|
|
|
"const ok = !process.env.FAILURE_KIND && !statusShort && process.env.LOCAL_HEAD && (!process.env.REMOTE_HEAD || process.env.LOCAL_HEAD === process.env.REMOTE_HEAD);",
|
|
|
|
|
|
|
|
"console.log(JSON.stringify({ ok, status: ok ? 'synced' : 'failed', workspace: process.env.WORKSPACE, branch: process.env.BRANCH || null, expectedBranch: process.env.EXPECTED_BRANCH, remoteName: process.env.REMOTE_NAME, remoteUrl: process.env.REMOTE_URL_AFTER || process.env.REMOTE_URL, localHead: process.env.LOCAL_HEAD || null, remoteHead: process.env.REMOTE_HEAD || null, remoteUpToDate: process.env.REMOTE_HEAD ? process.env.LOCAL_HEAD === process.env.REMOTE_HEAD : null, fetchExitCode: Number(process.env.FETCH_EXIT || '1'), updateExitCode: Number(process.env.UPDATE_EXIT || '1'), failureKind: process.env.FAILURE_KIND || null, statusShortLines: statusShort ? statusShort.split(/\\n/).filter(Boolean).length : 0, statusShortPreview: statusShort.replace(/[\\n\\t]+/g, ' ').slice(0, 1000) || null, stderrTail: process.env.STDERR_TAIL || null, valuesPrinted: false }));",
|
|
|
|
|
|
|
|
"NODE",
|
|
|
|
|
|
|
|
].join("\n");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nodeRuntimeSourceWorkspaceHostBootstrap(scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Record<string, unknown> {
|
|
|
|
function nodeRuntimeSourceWorkspaceHostBootstrap(scoped: ScopedNodeOptions, sourceWorkspace: HwlabRuntimeSourceWorkspaceSpec): Record<string, unknown> {
|
|
|
|
const stateDir = sourceWorkspace.install.stateDir;
|
|
|
|
const stateDir = sourceWorkspace.install.stateDir;
|
|
|
|
if (stateDir === undefined) {
|
|
|
|
if (stateDir === undefined) {
|
|
|
|