fix: make cicd in-cluster mode explicit

This commit is contained in:
Codex
2026-07-03 13:01:15 +00:00
parent 4b1b173b3f
commit 7c43716b3c
6 changed files with 30 additions and 18 deletions
+1
View File
@@ -42,6 +42,7 @@ bun scripts/cli.ts cicd branch-follower status
- 触发或验收 rollout 时必须绑定 lane、source commit、PipelineRun/GitOps revision、runtime ready 和 `/health` 端点验证结果;web-probe/Playwright 结果只能作为单独的 post-deploy 证据。
- CI/CD 状态、日志和事件查询必须减少 trans/SSH 传输:能在目标 NODE/k8s 内解析、聚合、裁剪的内容,必须在目标侧计算成短 JSON/table 摘要后再回传;禁止为了本地解析而把完整 ConfigMap、大对象、长日志或原始 API payload 透传回来。
- CI/CD 验证、测试和性能度量必须在目标 NODE/k8s 内执行,尤其是 branch-follower、Tekton/Argo、runtime reuse/env reuse、git mirror 和 runtime-ready 相关改动;不要在 master/local host 跑 test 或用本地验证结果替代目标运行面证据。本机只用于源码阅读、编辑和必要静态语法检查,正式收敛结论必须来自目标 NODE 计算出的短摘要。
- 一旦发现 CI/CD CLI 被误用且可能写入错误状态、产生伪证据或绕过目标运行面,必须立刻先把用法改成更符合直觉的公开入口并更新本 skill/reference,再继续验证或交付;不要只靠口头记忆、隐藏 flag、手动约定或后续小心来避免复发。内部 in-cluster 模式必须只由目标 k8s Job/Pod 调用,操作者从本机只能用公开入口提交目标侧 Job 或读取目标侧摘要。
- Secret 只通过 YAML sourceRef/targetKey 和受控 CLI 下发;输出只披露 presence/fingerprint。
- 长命令用异步 job 或短轮询;不要长时间挂住 trans/ssh。
@@ -93,4 +93,8 @@ Status readers must compute near the data. When the operator CLI reaches a targe
Validation, test and performance evidence for branch-follower changes must also run on the target NODE/k8s runtime, not on the local/master host. For CI/CD changes, use the target node's Tekton/Argo/runtime objects, controlled CLI jobs, and target-side summary scripts as the evidence source; local tests may not be cited as convergence or performance proof.
Operator-facing commands must use intuitive target-side verbs instead of internal execution flags. From a local/master host, use `status --live`, `run-once ...`, `events`, or `logs`; these commands create a bounded target-side Job when live state is needed. The internal `--in-cluster` flag is reserved for the Kubernetes Job/Pod command line after the registry, serviceaccount, in-cluster API endpoint and EmptyDir source checkout are mounted. It must not appear in user-facing examples.
Legacy `--controller` is accepted only as a compatibility spelling: inside Kubernetes it maps to `--in-cluster`, while outside Kubernetes it behaves like the ordinary public target-side path rather than running in-cluster logic locally. If an internal flag, hidden mode, or operator shortcut is misused and can write partial state or misleading evidence, stop feature work and simplify the public command semantics plus this reference before continuing.
`run-once --dry-run` is read-only for deployment: it may refresh the state ConfigMap with current native observations, but it must not trigger adapters.
+1 -1
View File
@@ -17,7 +17,7 @@ while true; do
git clone --branch "${UNIDESK_CONTROLLER_SOURCE_BRANCH}" "/cache/${UNIDESK_CONTROLLER_SOURCE_REPOSITORY}.git" /work/unidesk
cp /etc/unidesk-cicd-branch-follower/cicd-branch-followers.yaml /work/unidesk/config/cicd-branch-followers.yaml
cd /work/unidesk
bun scripts/cli.ts cicd branch-follower run-once --all --confirm --controller --config config/cicd-branch-followers.yaml --timeout-seconds "${timeout}" || true
bun scripts/cli.ts cicd branch-follower run-once --all --confirm --in-cluster --config config/cicd-branch-followers.yaml --timeout-seconds "${timeout}" || true
echo "branch-follower loop finished $(date -Iseconds)"
cd /work
sleep "${interval}"
+1 -1
View File
@@ -18,7 +18,7 @@ export function renderControllerReconcileJob(registry: BranchFollowerRegistry, o
...(options.followerId === null ? ["--all"] : ["--follower", options.followerId]),
mode.dryRun ? "--dry-run" : "--confirm",
"--wait",
"--controller",
"--in-cluster",
"--config",
"config/cicd-branch-followers.yaml",
"--timeout-seconds",
+1 -1
View File
@@ -23,7 +23,7 @@ export interface ParsedOptions {
confirm: boolean;
dryRun: boolean;
wait: boolean;
controller: boolean;
inCluster: boolean;
live: boolean;
noLive: boolean;
full: boolean;
+22 -15
View File
@@ -117,8 +117,11 @@ function parseOptions(args: string[]): ParsedOptions {
options.dryRun = true;
} else if (arg === "--wait") {
options.wait = true;
} else if (arg === "--in-cluster") {
options.inCluster = true;
} else if (arg === "--controller") {
options.controller = true;
if (isInClusterRuntime()) options.inCluster = true;
else if (options.action === "status") options.live = true;
} else if (arg === "--live") {
options.live = true;
} else if (arg === "--no-live") {
@@ -162,6 +165,10 @@ function parseOptions(args: string[]): ParsedOptions {
return options;
}
function isInClusterRuntime(): boolean {
return Boolean(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT);
}
function defaultOptions(action: BranchFollowerAction, _args: string[]): ParsedOptions {
return {
action,
@@ -171,7 +178,7 @@ function defaultOptions(action: BranchFollowerAction, _args: string[]): ParsedOp
confirm: false,
dryRun: false,
wait: false,
controller: false,
inCluster: false,
live: false,
noLive: false,
full: false,
@@ -525,9 +532,9 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed
async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
let k8s = readK8sState(registry, options);
const wantsLive = options.live || (!options.noLive && Object.keys(k8s.stateByFollower).length === 0);
const refresh = wantsLive && !options.controller ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null;
const refresh = wantsLive && !options.inCluster ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null;
if (refresh !== null) k8s = readK8sState(registry, options);
const shouldLive = wantsLive && options.controller;
const shouldLive = wantsLive && options.inCluster;
const selected = selectFollowers(registry, options, { includeDisabled: true });
const followers = [];
for (const follower of selected) {
@@ -553,7 +560,7 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti
}
async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
if (!options.controller) {
if (!options.inCluster) {
const refresh = runControllerReconcileJob(registry, options, { dryRun: options.dryRun, wait: true, recordState: true });
const k8s = readK8sState(registry, options);
const selected = selectFollowers(registry, options, { includeDisabled: false });
@@ -599,7 +606,7 @@ async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions)
dryRun: options.dryRun,
confirm: options.confirm,
wait: options.wait,
controller: options.controller,
controller: options.inCluster,
registry: registrySummary(registry),
followers: results,
warnings: stateWriteWarnings,
@@ -663,7 +670,7 @@ async function runFollowerDrillDown(registry: BranchFollowerRegistry, options: P
}
const follower = registry.followers.find((item) => item.id === options.followerId);
if (follower === undefined) throw new Error(`unknown follower ${options.followerId}`);
if (!options.controller) {
if (!options.inCluster) {
const refresh = runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true });
const k8s = readK8sState(registry, options);
const stored = k8s.stateByFollower[follower.id] ?? {};
@@ -781,7 +788,7 @@ async function decideAndMaybeTrigger(
if (options.confirm && (phase === "PendingTrigger" || phase === "Superseded" || (phase === "Observed" && observedSha !== null))) {
const trigger = await executeTrigger(registry, follower, observedSha, options);
triggerCommand = trigger.command;
phase = trigger.ok ? (options.wait || options.controller ? "ClosingOut" : "Triggering") : "Failed";
phase = trigger.ok ? (options.wait || options.inCluster ? "ClosingOut" : "Triggering") : "Failed";
decision = trigger.ok ? `trigger submitted for ${shortSha(observedSha)}` : `trigger failed for ${shortSha(observedSha)}`;
inFlightJob = trigger.jobId ?? live.inFlightJob;
lastTriggeredSha = observedSha;
@@ -839,7 +846,7 @@ async function decideAndMaybeTrigger(
inFlightJob,
budgetSource: follower.budgets,
controller: {
mode: options.controller ? "k8s-controller" : "local-cli",
mode: options.inCluster ? "k8s-controller" : "local-cli",
stateConfigMap: registry.controller.stateConfigMapName,
leaseName: registry.controller.leaseName,
},
@@ -861,16 +868,16 @@ async function decideAndMaybeTrigger(
async function executeTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions): Promise<TriggerResult> {
const spec = follower.commands.trigger;
const timeoutSeconds = options.timeoutSeconds ?? spec.timeoutSeconds;
if (follower.adapter === "hwlab-node-runtime" && options.controller) {
if (follower.adapter === "hwlab-node-runtime" && options.inCluster) {
return await executeNativeHwlabNodeTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
if (follower.adapter === "agentrun-yaml-lane" && options.controller) {
if (follower.adapter === "agentrun-yaml-lane" && options.inCluster) {
return await executeNativeAgentRunTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
if (follower.adapter === "web-probe-sentinel-cicd" && options.controller) {
if (follower.adapter === "web-probe-sentinel-cicd" && options.inCluster) {
return await executeNativeSentinelTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
if (!options.wait && !options.controller) {
if (!options.wait && !options.inCluster) {
const job = startJob(`cicd_branch_follower_${safeJobSegment(follower.id)}`, spec.argv, `Trigger ${follower.id} for observed sha ${observedSha ?? "unknown"}`);
return {
ok: true,
@@ -1957,7 +1964,7 @@ function kubePodList(registry: BranchFollowerRegistry, options: ParsedOptions, s
}
function runKubeScript(registry: BranchFollowerRegistry, options: ParsedOptions, script: string, input: string, timeoutMs: number): CommandResult {
if (options.controller) {
if (options.inCluster) {
return runCommand(["sh", "-lc", script], repoRoot, { input, timeoutMs });
}
return runCommand([transPath(), registry.controller.kubeRoute, "sh"], repoRoot, { input: `${script}\n`, timeoutMs });
@@ -2349,7 +2356,7 @@ function roundSeconds(value: number): number {
function writeFollowerState(registry: BranchFollowerRegistry, state: FollowerState, options: ParsedOptions): CommandResult {
const json = JSON.stringify(compactFollowerStateForConfigMap(state));
const dataPatch = JSON.stringify({ data: { [state.id]: json, _updatedAt: new Date().toISOString(), _specRef: SPEC_REF } });
if (options.controller) {
if (options.inCluster) {
const patchBase64 = Buffer.from(dataPatch, "utf8").toString("base64");
const createBase64 = Buffer.from(JSON.stringify({
metadata: {