diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index 2cd9df74..5d229dab 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -182,6 +182,8 @@ Cloud Web 单元测试必须自动发现并执行 repo-owned `web/hwlab-cloud-we Git 读写都走 `devops-infra` 本地 mirror/relay。读路径用 HTTP mirror checkout `v0.2` source 和 `v0.2-gitops` catalog;写路径把 promotion 推到 mirror/relay 的 `v0.2-gitops`,Argo 也从 mirror/relay 读取。这样 source clone、catalog fetch、GitOps clone/push 都落在集群内网络和本地磁盘,GitHub 只通过手动 `sync`/`flush` 进入或离开本地 mirror,不在 CI 关键路径内。 +任何仍需要访问 GitHub canonical remote 的 SSH 操作都必须显式走 G14 proxy。Host SSH config 可以使用 OpenBSD `nc -X connect -x 127.0.0.1:10808 %h %p`;Tekton `GIT_SSH_COMMAND`、git-mirror `sync`/`flush` 和其他容器内 `git@github.com` 或 `ssh://git@ssh.github.com:443` 路径必须使用 repo-owned Node HTTP CONNECT ProxyCommand,并保留短 `ConnectTimeout` 与 `ServerAlive*`。只改 `HTTP_PROXY`/`HTTPS_PROXY` 对 OpenSSH 无效;BusyBox `nc` 不支持 `-X connect`,不得再把 `ssh.github.com:443` 当作等价 proxy 或让 GitHub SSH 直连反复 60s 超时。 + env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境,`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。code-only 变更只更新 boot metadata 和 runtime identity,不发布新 env image;只有真实 env 输入变化或缺少可复用 env digest 时才进入 env rebuild。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash,`scripts` 只属于开发入口,不得因为清理旧门禁脚本而重建所有 service 或重建 env image。 BuildKit publish 采用 service 级全并行 fan-out。 diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index a7692d46..a98fe016 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -1090,11 +1090,69 @@ function gitSshShellFunction() { chmod 600 /root/.ssh/id_rsa timeout 10 ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true timeout 10 ssh-keyscan -p 443 ssh.github.com >> /root/.ssh/known_hosts 2>/dev/null || true - export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2" + write_github_proxy_command + export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'" git config --global url."ssh://git@ssh.github.com:443/".insteadOf "git@github.com:" git config --global url."ssh://git@ssh.github.com:443/".insteadOf "ssh://git@github.com/" } +write_github_proxy_command() { + cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' +#!/usr/bin/env node +import net from "node:net"; + +const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); +const proxyPort = Number.parseInt(proxyPortRaw || "", 10); +const targetPort = Number.parseInt(targetPortRaw || "", 10); +if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) { + console.error("usage: hwlab-github-proxy-connect "); + process.exit(64); +} + +const socket = net.createConnection({ host: proxyHost, port: proxyPort }); +let buffer = Buffer.alloc(0); + +socket.setTimeout(10000, () => { + console.error("proxy connect timeout " + proxyHost + ":" + proxyPort + " -> " + targetHost + ":" + targetPort); + socket.destroy(); + process.exit(65); +}); + +socket.on("connect", () => { + socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\r\nHost: " + targetHost + ":" + targetPort + "\r\nProxy-Connection: Keep-Alive\r\n\r\n"); +}); + +socket.on("error", (error) => { + console.error("proxy connect failed: " + error.message); + process.exit(66); +}); + +function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd === -1 && buffer.length < 8192) return; + const head = buffer.slice(0, headerEnd + 4).toString("latin1"); + const statusLine = head.split("\r\n", 1)[0] || ""; + const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); + if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { + console.error("proxy CONNECT rejected: " + (statusLine || "missing-status")); + socket.destroy(); + process.exit(67); + } + socket.off("data", onData); + socket.setTimeout(0); + const rest = buffer.slice(headerEnd + 4); + if (rest.length) process.stdout.write(rest); + process.stdin.pipe(socket); + socket.pipe(process.stdout); +} + +socket.on("data", onData); +socket.on("close", () => process.exit(0)); +NODE_PROXY + chmod 0700 /tmp/hwlab-github-proxy-connect.mjs +} + git_now_ms() { node -e 'console.log(Date.now())' 2>/dev/null || date +%s000 } @@ -3078,7 +3136,38 @@ fi trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa chmod 0400 /root/.ssh/id_rsa -export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1" +cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' +#!/usr/bin/env node +import net from "node:net"; +const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); +const proxyPort = Number.parseInt(proxyPortRaw || "", 10); +const targetPort = Number.parseInt(targetPortRaw || "", 10); +if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) process.exit(64); +const socket = net.createConnection({ host: proxyHost, port: proxyPort }); +let buffer = Buffer.alloc(0); +socket.setTimeout(10000, () => { socket.destroy(); process.exit(65); }); +socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\r\nHost: " + targetHost + ":" + targetPort + "\r\nProxy-Connection: Keep-Alive\r\n\r\n")); +socket.on("error", () => process.exit(66)); +function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd === -1 && buffer.length < 8192) return; + const head = buffer.slice(0, headerEnd + 4).toString("latin1"); + const statusLine = head.split("\r\n", 1)[0] || ""; + const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); + if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { socket.destroy(); process.exit(67); } + socket.off("data", onData); + socket.setTimeout(0); + const rest = buffer.slice(headerEnd + 4); + if (rest.length) process.stdout.write(rest); + process.stdin.pipe(socket); + socket.pipe(process.stdout); +} +socket.on("data", onData); +socket.on("close", () => process.exit(0)); +NODE_PROXY +chmod 0700 /tmp/hwlab-github-proxy-connect.mjs +export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'" if [ -d "$repo_path/objects" ] && [ -f "$repo_path/HEAD" ]; then git -C "$repo_path" remote set-url origin "$repo_url" || git -C "$repo_path" remote add origin "$repo_url" else @@ -3293,7 +3382,38 @@ trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM mkdir -p /root/.ssh cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa chmod 0400 /root/.ssh/id_rsa -export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1" +cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY' +#!/usr/bin/env node +import net from "node:net"; +const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2); +const proxyPort = Number.parseInt(proxyPortRaw || "", 10); +const targetPort = Number.parseInt(targetPortRaw || "", 10); +if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) process.exit(64); +const socket = net.createConnection({ host: proxyHost, port: proxyPort }); +let buffer = Buffer.alloc(0); +socket.setTimeout(10000, () => { socket.destroy(); process.exit(65); }); +socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\r\nHost: " + targetHost + ":" + targetPort + "\r\nProxy-Connection: Keep-Alive\r\n\r\n")); +socket.on("error", () => process.exit(66)); +function onData(chunk) { + buffer = Buffer.concat([buffer, chunk]); + const headerEnd = buffer.indexOf("\r\n\r\n"); + if (headerEnd === -1 && buffer.length < 8192) return; + const head = buffer.slice(0, headerEnd + 4).toString("latin1"); + const statusLine = head.split("\r\n", 1)[0] || ""; + const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10); + if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { socket.destroy(); process.exit(67); } + socket.off("data", onData); + socket.setTimeout(0); + const rest = buffer.slice(headerEnd + 4); + if (rest.length) process.stdout.write(rest); + process.stdin.pipe(socket); + socket.pipe(process.stdout); +} +socket.on("data", onData); +socket.on("close", () => process.exit(0)); +NODE_PROXY +chmod 0700 /tmp/hwlab-github-proxy-connect.mjs +export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'" /script/install-hooks.sh local_head=$(git -C "$repo_path" rev-parse refs/heads/v0.2-gitops 2>/dev/null || true) if [ -z "$local_head" ]; then