Merge remote-tracking branch 'origin/master'

This commit is contained in:
Codex
2026-07-09 14:53:19 +02:00
5 changed files with 119 additions and 19 deletions
+2
View File
@@ -96,6 +96,8 @@ targets:
redisPort: 16379
databaseHost: 127.0.0.1
databaseSslMode: require
preserveEnvKeys:
- API_KEY
noProxy:
- localhost
- 127.0.0.1
+1 -1
View File
@@ -99,7 +99,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code status --target NC01
NC01 `agentrun-nc01-v02` PaC lane 的正式触发链路是:GitHub PR 合并到 `pikasTech/agentrun@v0.2` -> GitHub webhook bridge 同步到 Gitea controlled mirror 和 immutable snapshot refs -> Gitea repository push webhook 触发 Pipelines-as-Code -> Tekton PipelineRun -> GitOps/Argo -> k8s runtime。该 lane 不再使用 branch-follower、Gitea Actions、act_runner 或自维护 `trigger-current` 作为正式 CI 触发器;对应 source authority、repo visibility、public exposure 和 mirror credentials 属于 `config/platform-infra/gitea.yaml`PaC controller/Repository/webhook/Tekton 参数属于 `config/platform-infra/pipelines-as-code.yaml`。Gitea 的公开 Web UI 是 `https://gitea.pikapython.com`,但 CI、Argo 和 runtime 内部读取必须使用 `gitea-http.devops-infra.svc.cluster.local:3000` 的 ClusterIP URL,避免公网回环和 legacy git-mirror commit 缺失。
AgentRun PaC lane 的 closeout 以 `bun scripts/cli.ts cicd status --node <NODE>``bun scripts/cli.ts platform-infra pipelines-as-code status --target <NODE> --consumer agentrun-<node>-v02` 为首选状态入口。默认输出必须能直接看到 webhook 是否存在、最新 PipelineRun/TaskRun duration、image status、env identity、digest、GitOps commit、Argo revision/health、manager Deployment readiness 和 runtime source/env annotation。env reuse 的通过证据是 `IMAGE_STATUS=reused`、同一 env identity 和稳定 digest;首次 cache miss 可以作为冷启动事实记录,但不能替代后续 env-reuse 秒级收口。若 GitHub PR 已合并但没有新 PipelineRun,应先排查 `platform-infra gitea mirror webhook status|test``platform-infra pipelines-as-code history`,不得改用 `trigger-current`、直接创建 PipelineRun、手工 push Gitea 或裸 `kubectl` 作为正式补触发。
AgentRun PaC lane 的 closeout 以 `bun scripts/cli.ts cicd status --node <NODE>``bun scripts/cli.ts platform-infra pipelines-as-code status --target <NODE> --consumer agentrun-<node>-v02` 为首选状态入口。默认输出必须能直接看到 webhook 是否存在、最新 PipelineRun/TaskRun duration、image status、env identity、digest、GitOps commit、Argo revision/health、manager Deployment readiness 和 runtime source/env annotation。env reuse 的通过证据是 `IMAGE_STATUS=reused`、同一 env identity 和稳定 digest;首次 cache miss 可以作为冷启动事实记录,但不能替代后续 env-reuse 秒级收口。若 GitHub PR 已合并但没有新 PipelineRun,应先排查 `platform-infra gitea mirror webhook status|test``platform-infra pipelines-as-code history`,不得改用 `trigger-current`、直接创建 PipelineRun、手工 push Gitea 或裸 `kubectl` 作为正式补触发。若 webhook delivery 返回成功但 GitHub head、Gitea branch、snapshot 或 latest PipelineRun source 不一致,先用 `platform-infra gitea mirror webhook status --target <NODE>` 披露差异,再用受控 `platform-infra gitea mirror sync --target <NODE> --confirm` 修复 mirror/snapshot,并把自动跟进缺口归并到 pikasTech/unidesk#1622;不要重复触发旧 `trigger-current` 掩盖 source authority 分叉。
YAML-only lane 的 `trigger-current --confirm` 是受控长流程入口;k8s git-mirror snapshot sync、image build、GitOps publish、git-mirror flush 和 PipelineRun 创建必须拆成短提交与状态轮询,不得把 clone、build、push 或长时间 polling 放进一个顶层 `trans` 长连接。`trigger-current` 返回异步 job 时,先用 `bun scripts/cli.ts job status <jobId> --tail-bytes 12000` 观察 `agentrun-yaml-lane-trigger` progress,再用 `agentrun control-plane status --node <node> --lane <lane> --pipeline-run <name>` 观察 Tekton、GitOps 和 Argo 对齐。后台步骤的 `status``ok` 必须共同判定,`status=succeeded``ok=false` 是终态失败,不能继续轮询到超时。GitOps publish 必须使用隔离临时 clone/worktree,不能切换或污染任何固定 source workspace`v0.2` 历史失败 publish 若留下 dirty/detached/GitOps branch 状态,不得通过 host workspace repair 恢复 source authority,只清理已知发布残留并从 git-mirror snapshot 重新触发。
+4
View File
@@ -224,6 +224,7 @@ export function parseHostDockerConfig(value: unknown, path: string, runtimeMode:
redisPort: integerField(record, "redisPort", `${path}.hostDocker`),
databaseHost: stringField(record, "databaseHost", `${path}.hostDocker`),
databaseSslMode: stringField(record, "databaseSslMode", `${path}.hostDocker`),
preserveEnvKeys: record.preserveEnvKeys === undefined ? [] : stringArrayField(record, "preserveEnvKeys", `${path}.hostDocker`),
noProxy: stringArrayField(record, "noProxy", `${path}.hostDocker`),
};
validateProxyName(hostDocker.projectName, `${path}.hostDocker.projectName`);
@@ -234,6 +235,9 @@ export function parseHostDockerConfig(value: unknown, path: string, runtimeMode:
validatePort(hostDocker.redisPort, `${path}.hostDocker.redisPort`);
validateHostOrIp(hostDocker.databaseHost, `${path}.hostDocker.databaseHost`);
if (!/^[A-Za-z0-9_-]+$/u.test(hostDocker.databaseSslMode)) throw new Error(`${configPath}.${path}.hostDocker.databaseSslMode has an unsupported format`);
for (const key of hostDocker.preserveEnvKeys) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw new Error(`${configPath}.${path}.hostDocker.preserveEnvKeys contains an unsupported env key`);
}
return hostDocker;
}
+1
View File
@@ -140,6 +140,7 @@ export interface Sub2ApiHostDockerConfig {
redisPort: number;
databaseHost: string;
databaseSslMode: string;
preserveEnvKeys: string[];
noProxy: string[];
}
+111 -18
View File
@@ -25,6 +25,7 @@ export function hostDockerPlanSummary(sub2api: Sub2ApiConfig, target: Sub2ApiTar
databaseHost: database.host,
databasePort: database.port,
databaseSslMode: database.sslMode,
preserveEnvKeys: host.preserveEnvKeys,
noProxy: host.noProxy,
valuesPrinted: false,
};
@@ -226,6 +227,7 @@ export function hostDockerApplyScript(sub2api: Sub2ApiConfig, target: Sub2ApiTar
const env = hostDockerEnvFile(sub2api, target, secretMaterial);
const composeB64 = Buffer.from(compose, "utf8").toString("base64");
const envB64 = Buffer.from(env, "utf8").toString("base64");
const preserveEnvKeysB64 = Buffer.from(host.preserveEnvKeys.join("\n"), "utf8").toString("base64");
const image = targetImage(sub2api, target);
const dependencyImages = targetDependencyImages(sub2api, target);
const pullCommand = image.pullPolicy === "Always"
@@ -246,10 +248,15 @@ tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
compose_tmp="$tmp/docker-compose.yml"
env_tmp="$tmp/sub2api.env"
preserve_keys="$tmp/preserve-env-keys.txt"
existing_env="$tmp/existing.env"
printf '%s' '${composeB64}' | base64 -d >"$compose_tmp"
printf '%s' '${envB64}' | base64 -d >"$env_tmp"
printf '%s' '${preserveEnvKeysB64}' | base64 -d >"$preserve_keys"
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
preserve_out="$tmp/preserve-env.out"
preserve_err="$tmp/preserve-env.err"
install_out="$tmp/install.out"
install_err="$tmp/install.err"
pull_out="$tmp/pull.out"
@@ -286,21 +293,100 @@ else
: >"$docker_out"
printf '%s\\n' 'docker daemon or docker compose is not accessible' >"$docker_err"
fi
preserve_rc=1
install_rc=1
if [ "$docker_rc" -eq 0 ]; then
sudo mkdir -p ${shQuote(host.workDir)} ${shQuote(host.appDataDir)} "$(dirname ${shQuote(host.composePath)})" "$(dirname ${shQuote(host.envPath)})" >"$install_out" 2>"$install_err"
install_rc=$?
if [ "$install_rc" -eq 0 ]; then
sudo install -m 0644 "$compose_tmp" ${shQuote(host.composePath)} >>"$install_out" 2>>"$install_err"
install_rc=$?
preserve_rc=0
: >"$existing_env"
if [ -s "$preserve_keys" ] && [ -f ${shQuote(host.envPath)} ]; then
if ! cat ${shQuote(host.envPath)} >"$existing_env" 2>"$preserve_err"; then
if ! sudo cat ${shQuote(host.envPath)} >"$existing_env" 2>>"$preserve_err"; then
preserve_rc=1
fi
fi
else
: >"$preserve_err"
fi
if [ "$install_rc" -eq 0 ]; then
current_uid="$(id -u)"
current_gid="$(id -g)"
sudo install -m 0600 -o "$current_uid" -g "$current_gid" "$env_tmp" ${shQuote(host.envPath)} >>"$install_out" 2>>"$install_err"
if [ "$preserve_rc" -eq 0 ]; then
python3 - "$env_tmp" "$existing_env" "$preserve_keys" >"$preserve_out" 2>"$preserve_err" <<'PY'
import json
import re
import sys
env_path, existing_path, keys_path = sys.argv[1:]
keys = []
with open(keys_path, encoding="utf-8", errors="replace") as handle:
for line in handle:
key = line.strip()
if key:
keys.append(key)
existing = {}
try:
lines = open(existing_path, encoding="utf-8", errors="replace").read().splitlines()
except FileNotFoundError:
lines = []
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key = stripped.split("=", 1)[0].strip()
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key):
existing[key] = line
current = open(env_path, encoding="utf-8", errors="replace").read()
current_keys = set()
for line in current.splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("#") and "=" in stripped:
current_keys.add(stripped.split("=", 1)[0].strip())
append_lines = []
missing = []
skipped_present = []
for key in keys:
if key in current_keys:
skipped_present.append(key)
elif key in existing:
append_lines.append(existing[key])
else:
missing.append(key)
if append_lines:
with open(env_path, "a", encoding="utf-8") as handle:
if not current.endswith("\\n"):
handle.write("\\n")
handle.write("\\n".join(append_lines) + "\\n")
print(json.dumps({
"ok": True,
"keys": keys,
"preservedKeys": [line.split("=", 1)[0] for line in append_lines],
"missingKeys": missing,
"skippedAlreadyRenderedKeys": skipped_present,
"valuesPrinted": False,
}, ensure_ascii=False))
PY
preserve_rc=$?
else
printf '{"ok":false,"error":"read-existing-env-failed","valuesPrinted":false}\\n' >"$preserve_out"
fi
if [ "$preserve_rc" -eq 0 ]; then
sudo mkdir -p ${shQuote(host.workDir)} ${shQuote(host.appDataDir)} "$(dirname ${shQuote(host.composePath)})" "$(dirname ${shQuote(host.envPath)})" >"$install_out" 2>"$install_err"
install_rc=$?
if [ "$install_rc" -eq 0 ]; then
sudo install -m 0644 "$compose_tmp" ${shQuote(host.composePath)} >>"$install_out" 2>>"$install_err"
install_rc=$?
fi
if [ "$install_rc" -eq 0 ]; then
current_uid="$(id -u)"
current_gid="$(id -g)"
sudo install -m 0600 -o "$current_uid" -g "$current_gid" "$env_tmp" ${shQuote(host.envPath)} >>"$install_out" 2>>"$install_err"
install_rc=$?
fi
else
: >"$install_out"
printf '%s\\n' 'skipped because preserve env failed' >"$install_err"
install_rc=1
fi
else
: >"$preserve_out"
: >"$preserve_err"
: >"$install_out"
: >"$install_err"
fi
@@ -355,13 +441,13 @@ else
: >"$health_out"
printf '%s\\n' 'skipped because compose up failed' >"$health_err"
fi
python3 - "$docker_rc" "$install_rc" "$pull_rc" "$up_rc" "$health_rc" "$compose_mode" "$docker_out" "$docker_err" "$install_out" "$install_err" "$pull_out" "$pull_err" "$up_out" "$up_err" "$health_out" "$health_err" <<'PY'
python3 - "$docker_rc" "$preserve_rc" "$install_rc" "$pull_rc" "$up_rc" "$health_rc" "$compose_mode" "$docker_out" "$docker_err" "$preserve_out" "$preserve_err" "$install_out" "$install_err" "$pull_out" "$pull_err" "$up_out" "$up_err" "$health_out" "$health_err" <<'PY'
import json
import sys
docker_rc, install_rc, pull_rc, up_rc, health_rc = [int(value) for value in sys.argv[1:6]]
docker_mode = sys.argv[6]
paths = sys.argv[7:]
docker_rc, preserve_rc, install_rc, pull_rc, up_rc, health_rc = [int(value) for value in sys.argv[1:7]]
docker_mode = sys.argv[7]
paths = sys.argv[8:]
def text(path, limit=4000):
try:
@@ -369,8 +455,14 @@ def text(path, limit=4000):
except FileNotFoundError:
return ""
def parsed(path):
try:
return json.load(open(path, encoding="utf-8", errors="replace"))
except Exception:
return None
payload = {
"ok": docker_rc == 0 and install_rc == 0 and pull_rc == 0 and up_rc == 0 and health_rc == 0,
"ok": docker_rc == 0 and preserve_rc == 0 and install_rc == 0 and pull_rc == 0 and up_rc == 0 and health_rc == 0,
"target": "${target.id}",
"runtimeMode": "host-docker",
"route": "${target.route}",
@@ -387,10 +479,11 @@ payload = {
},
"steps": {
"docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
"installFiles": {"exitCode": install_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
"pull": {"exitCode": pull_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
"composeUp": {"exitCode": up_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
"localHealth": {"exitCode": health_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
"preserveEnv": {"exitCode": preserve_rc, "summary": parsed(paths[2]), "stdout": text(paths[2]), "stderr": text(paths[3])},
"installFiles": {"exitCode": install_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
"pull": {"exitCode": pull_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
"composeUp": {"exitCode": up_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
"localHealth": {"exitCode": health_rc, "stdout": text(paths[10]), "stderr": text(paths[11])},
},
"valuesPrinted": False,
}