fix: sync HWLAB source workspace through YAML
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
// Responsibility: YAML-first D518/D601 HWLAB source workspace status and bootstrap for web-probe clients.
|
||||
import { createHash } from "node:crypto";
|
||||
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 { hwlabRuntimeLaneConfigPath, type HwlabRuntimeLaneSpec, type HwlabRuntimeSourceWorkspaceHostDependenciesSpec, type HwlabRuntimeSourceWorkspaceSpec } from "../hwlab-node-lanes";
|
||||
import { parseNodeScopedDelegatedOptions } from "./plan";
|
||||
@@ -10,10 +12,10 @@ import { compactRuntimeCommand, parseLastJsonLineObject } from "./runtime-common
|
||||
import { commaListField, keyValueLinesFromText, numericField, shellQuote, statusText } from "./utils";
|
||||
|
||||
type ScopedNodeOptions = ReturnType<typeof parseNodeScopedDelegatedOptions>;
|
||||
type SourceWorkspaceAction = "status" | "bootstrap" | "host-deps";
|
||||
type SourceWorkspaceAction = "status" | "sync" | "bootstrap" | "host-deps";
|
||||
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 sourceWorkspace = scoped.spec.sourceWorkspace;
|
||||
if (sourceWorkspace === undefined) {
|
||||
@@ -29,18 +31,22 @@ export function nodeRuntimeSourceWorkspaceCommand(scoped: ScopedNodeOptions): Re
|
||||
};
|
||||
}
|
||||
if (action === "host-deps") return nodeRuntimeSourceWorkspaceHostDependencies(scoped, sourceWorkspace);
|
||||
if (action === "sync") return await nodeRuntimeSourceWorkspaceSync(config, scoped, sourceWorkspace);
|
||||
if (action === "status") return nodeRuntimeSourceWorkspaceStatus(scoped, sourceWorkspace);
|
||||
return nodeRuntimeSourceWorkspaceBootstrap(scoped, sourceWorkspace);
|
||||
}
|
||||
|
||||
function sourceWorkspaceAction(scoped: ScopedNodeOptions): SourceWorkspaceAction {
|
||||
const value = scoped.originalArgs[1];
|
||||
if (value !== "status" && 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]");
|
||||
if (value !== "status" && value !== "sync" && value !== "bootstrap" && value !== "host-deps") {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
const stateDir = sourceWorkspace.install.stateDir;
|
||||
if (stateDir === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user