@@ -54,26 +54,3 @@ spec:
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: apply-public-edge
|
||||
image: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
|
||||
imagePullPolicy: IfNotPresent
|
||||
workingDir: /workspace/source
|
||||
env:
|
||||
- name: SOURCE_COMMIT
|
||||
value: "{{revision}}"
|
||||
- name: UNIDESK_SSH_CLIENT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: public-edge-trans
|
||||
key: UNIDESK_SSH_CLIENT_TOKEN
|
||||
script: |
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
test "$(git rev-parse HEAD)" = "$SOURCE_COMMIT"
|
||||
exec bun scripts/native/cicd/deliver-platform-infra-public-edge.mjs \
|
||||
--source-root /workspace/source \
|
||||
--source-commit "$SOURCE_COMMIT" \
|
||||
--confirm
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
|
||||
@@ -6,31 +6,10 @@ metadata:
|
||||
owner: platform-infra
|
||||
relatedIssues:
|
||||
- 2181
|
||||
- 2205
|
||||
|
||||
defaults:
|
||||
targetId: NC01
|
||||
|
||||
delivery:
|
||||
enabled: true
|
||||
pipelineId: platform-infra-gitea-nc01
|
||||
sourceCommitEnvironment: SOURCE_COMMIT
|
||||
transport:
|
||||
hostCwd: /root
|
||||
secretRef:
|
||||
configRef: config/secrets-distribution.yaml
|
||||
targetId: public-edge-delivery-nc01
|
||||
secretName: public-edge-trans
|
||||
targetKey: UNIDESK_SSH_CLIENT_TOKEN
|
||||
hostHelper:
|
||||
archiveRoot: /root/unidesk/.state/public-edge-delivery
|
||||
entrypoint: scripts/native/cicd/apply-platform-infra-public-edge-archive.mjs
|
||||
sourcePaths:
|
||||
- config
|
||||
- scripts
|
||||
- package.json
|
||||
- bun.lock
|
||||
|
||||
targets:
|
||||
NC01:
|
||||
route: NC01
|
||||
|
||||
@@ -10,7 +10,6 @@ metadata:
|
||||
- 313
|
||||
- 2256
|
||||
- 1964
|
||||
- 2205
|
||||
|
||||
sources:
|
||||
root: /root/.unidesk/.state/secrets
|
||||
@@ -143,12 +142,6 @@ sources:
|
||||
- DATABASE_URL
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
- sourceRef: unidesk-host-k8s.env
|
||||
type: env
|
||||
requiredKeys:
|
||||
- UNIDESK_SSH_CLIENT_TOKEN
|
||||
createIfMissing:
|
||||
enabled: false
|
||||
externalFiles:
|
||||
- sourceRef: ~/.unidesk/.env/pikaoa-admin-password.txt
|
||||
type: raw-file
|
||||
@@ -257,21 +250,8 @@ targets:
|
||||
namespace: pikaoa-test
|
||||
scope: pikaoa-test
|
||||
enabled: true
|
||||
- id: public-edge-delivery-nc01
|
||||
route: NC01:k3s
|
||||
namespace: devops-infra
|
||||
scope: public-edge-delivery
|
||||
enabled: true
|
||||
|
||||
kubernetesSecrets:
|
||||
- name: public-edge-trans
|
||||
targetId: public-edge-delivery-nc01
|
||||
secretName: public-edge-trans
|
||||
type: Opaque
|
||||
data:
|
||||
- sourceRef: unidesk-host-k8s.env
|
||||
sourceKey: UNIDESK_SSH_CLIENT_TOKEN
|
||||
targetKey: UNIDESK_SSH_CLIENT_TOKEN
|
||||
- name: pikaoa-runtime
|
||||
targetId: pikaoa-nc01
|
||||
secretName: pikaoa-runtime
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
#### R9.5.1 [in_progress]
|
||||
|
||||
重建 [UniDesk #2205](https://github.com/pikasTech/unidesk/issues/2205):从最新 master 以全新 worktree/session 实现 NC01 shared public-edge 的 merge 驱动自动交付,复用同一 renderer/apply 与 owning YAML/source commit;禁止复用旧 docker.sock 工件,禁止 Kubernetes 挂载 Docker socket/daemon、第二 edge/authority、人工交付、PK01和Vitest,合并后只由正常自动链收敛,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R9.5.1_Task_Report.md)。
|
||||
##### R9.5.1.1 [completed]
|
||||
##### R9.5.1.1 [in_progress]
|
||||
|
||||
重做 [UniDesk #2205](https://github.com/pikasTech/unidesk/issues/2205) 的单一 shared public-edge 自动交付链:仅允许 immutable PaC checkout → YAML SecretRef → 受控 trans → NC01 host-local 既有 public-edge renderer/apply;禁止 Argo PostSync、hostPath、Caddy Admin API 直写、Docker socket/daemon、第二 authority/fallback、PK01、Vitest和人工终态交付;必须使用最新 master 的全新 worktree 与全新 Artificer session,合并后只由自动 CI/CD 收敛,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R9.5.1.1_Task_Report.md)。
|
||||
### R9.6 [completed]
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
const options = parseOptions(process.argv.slice(2));
|
||||
const sourceRoot = realpathSync(options.sourceRoot);
|
||||
const markerPath = join(dirname(sourceRoot), ".unidesk-source-commit");
|
||||
const markerCommit = readFileSync(markerPath, "utf8").trim();
|
||||
if (markerCommit !== options.sourceCommit) fail("archive source commit marker 不匹配");
|
||||
const configPath = resolve(sourceRoot, options.config);
|
||||
if (!configPath.startsWith(`${sourceRoot}/`)) fail("--config 必须位于 source archive 内");
|
||||
|
||||
const args = [
|
||||
"scripts/cli.ts",
|
||||
"platform-infra",
|
||||
"public-edge",
|
||||
"apply",
|
||||
"--config",
|
||||
configPath,
|
||||
"--target",
|
||||
options.target,
|
||||
"--confirm",
|
||||
];
|
||||
const result = spawnSync("bun", args, {
|
||||
cwd: sourceRoot,
|
||||
env: { ...process.env, UNIDESK_TRANS_REPO_ROOT: sourceRoot },
|
||||
stdio: "inherit",
|
||||
timeout: 120_000,
|
||||
});
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
|
||||
function parseOptions(args) {
|
||||
const values = new Map();
|
||||
let confirm = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm") {
|
||||
confirm = true;
|
||||
continue;
|
||||
}
|
||||
if (!["--source-root", "--source-commit", "--config", "--target"].includes(arg)) fail(`不支持的参数:${arg}`);
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) fail(`${arg} 需要参数`);
|
||||
values.set(arg.slice(2), value);
|
||||
index += 1;
|
||||
}
|
||||
const sourceRoot = values.get("source-root");
|
||||
const sourceCommit = values.get("source-commit");
|
||||
const config = values.get("config");
|
||||
const target = values.get("target");
|
||||
if (!confirm) fail("必须显式传入 --confirm");
|
||||
if (typeof sourceRoot !== "string" || !isAbsolute(sourceRoot)) fail("--source-root 必须是绝对路径");
|
||||
if (typeof sourceCommit !== "string" || !/^[0-9a-f]{40}$/u.test(sourceCommit)) fail("--source-commit 必须是 40 位 commit");
|
||||
if (typeof config !== "string" || isAbsolute(config)) fail("--config 必须是 archive 内相对路径");
|
||||
if (typeof target !== "string" || target.length === 0) fail("缺少 --target");
|
||||
return { sourceRoot, sourceCommit, config, target };
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`${JSON.stringify({ ok: false, action: "platform-infra-public-edge-archive-apply", error: message })}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
const options = parseOptions(process.argv.slice(2));
|
||||
const sourceRoot = resolve(options.sourceRoot);
|
||||
const configPath = resolve(sourceRoot, options.config);
|
||||
const config = Bun.YAML.parse(readFileSync(configPath, "utf8"));
|
||||
const plan = readPlan(config, sourceRoot, options.sourceCommit, configPath);
|
||||
verifySourceCommit(sourceRoot, options.sourceCommit, plan.sourceCommitEnvironment);
|
||||
|
||||
if (options.dryRun) {
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
action: "platform-infra-public-edge-delivery-plan",
|
||||
mutation: false,
|
||||
sourceCommit: options.sourceCommit,
|
||||
...plan.summary,
|
||||
valuesPrinted: false,
|
||||
}, null, 2)}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const token = process.env[plan.secret.targetKey]?.trim() ?? "";
|
||||
if (token.length === 0) fail(`缺少 SecretRef 投影环境变量:${plan.secret.targetKey}`);
|
||||
|
||||
const temporaryRoot = mkdtempSync(join(tmpdir(), "unidesk-public-edge-delivery-"));
|
||||
try {
|
||||
const archivePath = createArchive(sourceRoot, options.sourceCommit, plan.sourcePaths, temporaryRoot);
|
||||
const archiveSha256 = sha256File(archivePath);
|
||||
const route = `${plan.route}:${plan.hostCwd}`;
|
||||
const remoteArchive = `${plan.archiveRoot}/incoming/public-edge-${options.sourceCommit}.tar.gz`;
|
||||
const transPath = join(sourceRoot, "scripts/trans");
|
||||
const transEnv = {
|
||||
...process.env,
|
||||
UNIDESK_TRANS_REPO_ROOT: sourceRoot,
|
||||
};
|
||||
|
||||
runTrans(transPath, [route, "sh", "--", `umask 077; mkdir -p ${shellQuote(`${plan.archiveRoot}/incoming`)} ${shellQuote(`${plan.archiveRoot}/tmp`)}`], transEnv);
|
||||
const upload = runTrans(transPath, [route, "upload", archivePath, remoteArchive], transEnv);
|
||||
const remote = runTrans(transPath, [route, "sh"], transEnv, remoteApplyScript({
|
||||
archivePath: remoteArchive,
|
||||
archiveRoot: plan.archiveRoot,
|
||||
archiveSha256,
|
||||
config: options.config,
|
||||
entrypoint: plan.entrypoint,
|
||||
sourceCommit: options.sourceCommit,
|
||||
targetId: plan.targetId,
|
||||
}));
|
||||
const runtime = compactResult(remote);
|
||||
if (runtime.ok === false || runtime.mutation !== true) fail("host-local public-edge apply 未返回 mutation=true");
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
action: "platform-infra-public-edge-delivery",
|
||||
mutation: true,
|
||||
sourceCommit: options.sourceCommit,
|
||||
archive: {
|
||||
sha256: `sha256:${archiveSha256}`,
|
||||
sourcePaths: plan.sourcePaths,
|
||||
remotePath: remoteArchive,
|
||||
},
|
||||
transport: compactResult(upload),
|
||||
runtime,
|
||||
...plan.summary,
|
||||
valuesPrinted: false,
|
||||
}, null, 2)}\n`);
|
||||
} finally {
|
||||
rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function parseOptions(args) {
|
||||
let sourceRoot = null;
|
||||
let sourceCommit = null;
|
||||
let config = "config/platform-infra/public-edge.yaml";
|
||||
let dryRun = false;
|
||||
let confirm = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--dry-run") dryRun = true;
|
||||
else if (arg === "--confirm") confirm = true;
|
||||
else if (arg === "--source-root" || arg === "--source-commit" || arg === "--config") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) fail(`${arg} 需要参数`);
|
||||
if (arg === "--source-root") sourceRoot = value;
|
||||
else if (arg === "--source-commit") sourceCommit = value;
|
||||
else config = value;
|
||||
index += 1;
|
||||
} else fail(`不支持的参数:${arg}`);
|
||||
}
|
||||
if (sourceRoot === null) fail("缺少 --source-root");
|
||||
if (sourceCommit === null || !/^[0-9a-f]{40}$/u.test(sourceCommit)) fail("--source-commit 必须是 40 位小写十六进制 commit");
|
||||
if (dryRun === confirm) fail("必须且只能选择 --dry-run 或 --confirm");
|
||||
return { sourceRoot, sourceCommit, config, dryRun, confirm };
|
||||
}
|
||||
|
||||
function readPlan(config, sourceRoot, sourceCommit, configPath) {
|
||||
const delivery = record(config.delivery, "delivery");
|
||||
if (delivery.enabled !== true) fail("delivery.enabled 必须为 true");
|
||||
const defaults = record(config.defaults, "defaults");
|
||||
const targets = record(config.targets, "targets");
|
||||
const targetId = string(defaults.targetId, "defaults.targetId");
|
||||
const target = record(targets[targetId], `targets.${targetId}`);
|
||||
const route = string(target.route, `targets.${targetId}.route`);
|
||||
const transport = record(delivery.transport, "delivery.transport");
|
||||
const secret = record(transport.secretRef, "delivery.transport.secretRef");
|
||||
const hostHelper = record(delivery.hostHelper, "delivery.hostHelper");
|
||||
const sourcePaths = array(hostHelper.sourcePaths, "delivery.hostHelper.sourcePaths").map((value, index) => safeRelativePath(value, `delivery.hostHelper.sourcePaths[${index}]`));
|
||||
const entrypoint = safeRelativePath(hostHelper.entrypoint, "delivery.hostHelper.entrypoint");
|
||||
if (!sourcePaths.some((value) => entrypoint === value || entrypoint.startsWith(`${value}/`))) fail("hostHelper.entrypoint 必须包含在 sourcePaths 中");
|
||||
const configRelative = relativeWithin(sourceRoot, configPath);
|
||||
if (!sourcePaths.some((value) => configRelative === value || configRelative.startsWith(`${value}/`))) fail("public-edge config 必须包含在 sourcePaths 中");
|
||||
const secretSummary = {
|
||||
configRef: safeRelativePath(secret.configRef, "delivery.transport.secretRef.configRef"),
|
||||
targetId: string(secret.targetId, "delivery.transport.secretRef.targetId"),
|
||||
secretName: string(secret.secretName, "delivery.transport.secretRef.secretName"),
|
||||
targetKey: string(secret.targetKey, "delivery.transport.secretRef.targetKey"),
|
||||
};
|
||||
const secretBinding = readSecretBinding(sourceRoot, secretSummary);
|
||||
const archiveRoot = absolutePath(hostHelper.archiveRoot, "delivery.hostHelper.archiveRoot");
|
||||
return {
|
||||
sourceCommitEnvironment: string(delivery.sourceCommitEnvironment, "delivery.sourceCommitEnvironment"),
|
||||
targetId,
|
||||
route,
|
||||
hostCwd: absolutePath(transport.hostCwd, "delivery.transport.hostCwd"),
|
||||
archiveRoot,
|
||||
entrypoint,
|
||||
sourcePaths,
|
||||
secret: secretSummary,
|
||||
summary: {
|
||||
pipelineId: string(delivery.pipelineId, "delivery.pipelineId"),
|
||||
configRef: `${configRelative}#targets.${targetId}`,
|
||||
target: { id: targetId, route },
|
||||
secretRef: { ...secretSummary, sourceRef: secretBinding.sourceRef, sourceKey: secretBinding.sourceKey },
|
||||
hostHelper: { archiveRoot, entrypoint, sourcePaths },
|
||||
sourceCommit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readSecretBinding(sourceRoot, secret) {
|
||||
const configPath = resolve(sourceRoot, secret.configRef);
|
||||
const config = Bun.YAML.parse(readFileSync(configPath, "utf8"));
|
||||
const targets = array(config.targets, `${secret.configRef}#targets`);
|
||||
const target = targets.find((value) => record(value, "secret target").id === secret.targetId);
|
||||
if (target === undefined || record(target, "secret target").enabled !== true) fail(`Secret target 未启用:${secret.targetId}`);
|
||||
const bindings = array(config.kubernetesSecrets, `${secret.configRef}#kubernetesSecrets`);
|
||||
const binding = bindings.find((value) => record(value, "secret binding").name === secret.secretName);
|
||||
if (binding === undefined) fail(`Secret binding 不存在:${secret.secretName}`);
|
||||
const bindingRecord = record(binding, `kubernetesSecrets.${secret.secretName}`);
|
||||
if (bindingRecord.targetId !== secret.targetId || bindingRecord.secretName !== secret.secretName) fail(`Secret binding target 不匹配:${secret.secretName}`);
|
||||
const data = array(bindingRecord.data, `kubernetesSecrets.${secret.secretName}.data`);
|
||||
const item = data.find((value) => record(value, "secret data").targetKey === secret.targetKey);
|
||||
if (item === undefined) fail(`Secret targetKey 不存在:${secret.targetKey}`);
|
||||
const itemRecord = record(item, `kubernetesSecrets.${secret.secretName}.data.${secret.targetKey}`);
|
||||
return {
|
||||
sourceRef: string(itemRecord.sourceRef, `kubernetesSecrets.${secret.secretName}.data.sourceRef`),
|
||||
sourceKey: string(itemRecord.sourceKey, `kubernetesSecrets.${secret.secretName}.data.sourceKey`),
|
||||
};
|
||||
}
|
||||
|
||||
function verifySourceCommit(sourceRoot, sourceCommit, environmentName) {
|
||||
const environmentCommit = process.env[environmentName]?.trim() ?? "";
|
||||
if (environmentCommit !== sourceCommit) fail(`${environmentName} 与 --source-commit 不一致`);
|
||||
const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: sourceRoot, encoding: "utf8" });
|
||||
if (result.status !== 0) fail(`无法读取 source checkout commit:${result.stderr || result.stdout}`);
|
||||
if (result.stdout.trim() !== sourceCommit) fail("source checkout HEAD 与 --source-commit 不一致");
|
||||
}
|
||||
|
||||
function createArchive(sourceRoot, sourceCommit, sourcePaths, temporaryRoot) {
|
||||
const tarPath = join(temporaryRoot, "source.tar");
|
||||
const markerPath = join(temporaryRoot, ".unidesk-source-commit");
|
||||
writeFileSync(markerPath, `${sourceCommit}\n`, { mode: 0o600 });
|
||||
runChecked("git", ["archive", "--format=tar", "--prefix=source/", `--output=${tarPath}`, sourceCommit, "--", ...sourcePaths], sourceRoot);
|
||||
runChecked("tar", ["--append", "--file", tarPath, "--directory", temporaryRoot, ".unidesk-source-commit"], sourceRoot);
|
||||
runChecked("gzip", ["-n", tarPath], sourceRoot);
|
||||
return `${tarPath}.gz`;
|
||||
}
|
||||
|
||||
function remoteApplyScript(input) {
|
||||
return `set -eu
|
||||
archive=${shellQuote(input.archivePath)}
|
||||
archive_root=${shellQuote(input.archiveRoot)}
|
||||
expected_sha=${shellQuote(input.archiveSha256)}
|
||||
source_commit=${shellQuote(input.sourceCommit)}
|
||||
entrypoint=${shellQuote(input.entrypoint)}
|
||||
config=${shellQuote(input.config)}
|
||||
target_id=${shellQuote(input.targetId)}
|
||||
actual_sha="$(sha256sum "$archive" | awk '{print $1}')"
|
||||
test "$actual_sha" = "$expected_sha"
|
||||
tmp="$(mktemp -d "$archive_root/tmp/$source_commit.XXXXXX")"
|
||||
cleanup() { rm -rf "$tmp"; rm -f "$archive"; }
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
tar -xzf "$archive" -C "$tmp"
|
||||
test "$(cat "$tmp/.unidesk-source-commit")" = "$source_commit"
|
||||
test -f "$tmp/source/$entrypoint"
|
||||
cd "$tmp/source"
|
||||
bun "$entrypoint" --source-root "$tmp/source" --source-commit "$source_commit" --config "$config" --target "$target_id" --confirm
|
||||
`;
|
||||
}
|
||||
|
||||
function runTrans(transPath, args, env, input) {
|
||||
const result = spawnSync(transPath, args, { encoding: "utf8", env, input, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 });
|
||||
if (result.status !== 0) fail(`trans ${args.slice(1, 3).join(" ")} 失败:${result.stderr || result.stdout}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function runChecked(command, args, cwd) {
|
||||
const result = spawnSync(command, args, { cwd, encoding: "utf8", timeout: 60_000, maxBuffer: 16 * 1024 * 1024 });
|
||||
if (result.status !== 0) fail(`${command} ${args.join(" ")} 失败:${result.stderr || result.stdout}`);
|
||||
}
|
||||
|
||||
function compactResult(result) {
|
||||
const stdout = result.stdout.trim();
|
||||
if (stdout.length === 0) return { ok: true };
|
||||
try {
|
||||
return JSON.parse(stdout);
|
||||
} catch {
|
||||
return { ok: true, stdout: stdout.slice(-2000) };
|
||||
}
|
||||
}
|
||||
|
||||
function sha256File(path) {
|
||||
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
||||
}
|
||||
|
||||
function record(value, label) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) fail(`${label} 必须是对象`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function array(value, label) {
|
||||
if (!Array.isArray(value) || value.length === 0) fail(`${label} 必须是非空数组`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value, label) {
|
||||
if (typeof value !== "string" || value.trim().length === 0) fail(`${label} 必须是非空字符串`);
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function absolutePath(value, label) {
|
||||
const path = string(value, label);
|
||||
if (!isAbsolute(path)) fail(`${label} 必须是绝对路径`);
|
||||
return path.replace(/\/+$/u, "") || "/";
|
||||
}
|
||||
|
||||
function safeRelativePath(value, label) {
|
||||
const path = string(value, label);
|
||||
if (isAbsolute(path) || !/^[A-Za-z0-9._/-]+$/u.test(path) || path.split("/").some((segment) => segment === ".." || segment === "")) fail(`${label} 必须是仓库内相对路径`);
|
||||
return path;
|
||||
}
|
||||
|
||||
function relativeWithin(root, path) {
|
||||
const prefix = `${root.replace(/\/+$/u, "")}/`;
|
||||
if (!path.startsWith(prefix)) fail("--config 必须位于 --source-root 内");
|
||||
return path.slice(prefix.length);
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/gu, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
process.stderr.write(`${JSON.stringify({ ok: false, action: "platform-infra-public-edge-delivery", error: message })}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { rootPath } from "../../src/config";
|
||||
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("platform-infra public-edge automatic delivery", () => {
|
||||
test("renders a commit-pinned single-authority dry-run plan", () => {
|
||||
const root = rootPath();
|
||||
const sourceCommit = gitHead(root);
|
||||
const result = spawnSync("bun", [
|
||||
"scripts/native/cicd/deliver-platform-infra-public-edge.mjs",
|
||||
"--source-root", root,
|
||||
"--source-commit", sourceCommit,
|
||||
"--dry-run",
|
||||
], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, SOURCE_COMMIT: sourceCommit },
|
||||
timeout: 30_000,
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(JSON.parse(result.stdout)).toMatchObject({
|
||||
ok: true,
|
||||
action: "platform-infra-public-edge-delivery-plan",
|
||||
mutation: false,
|
||||
sourceCommit,
|
||||
pipelineId: "platform-infra-gitea-nc01",
|
||||
target: { id: "NC01", route: "NC01" },
|
||||
secretRef: {
|
||||
configRef: "config/secrets-distribution.yaml",
|
||||
targetId: "public-edge-delivery-nc01",
|
||||
secretName: "public-edge-trans",
|
||||
targetKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
sourceRef: "unidesk-host-k8s.env",
|
||||
sourceKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
},
|
||||
hostHelper: {
|
||||
entrypoint: "scripts/native/cicd/apply-platform-infra-public-edge-archive.mjs",
|
||||
sourcePaths: ["config", "scripts", "package.json", "bun.lock"],
|
||||
},
|
||||
valuesPrinted: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("PaC projects only the controlled trans secret and existing apply entrypoint", () => {
|
||||
const pac = readFileSync(`${rootPath()}/.tekton/platform-infra-gitea-nc01-pac.yaml`, "utf8");
|
||||
expect(pac).toContain("name: apply-public-edge");
|
||||
expect(pac).toContain("name: public-edge-trans");
|
||||
expect(pac).toContain("key: UNIDESK_SSH_CLIENT_TOKEN");
|
||||
expect(pac).toContain("deliver-platform-infra-public-edge.mjs");
|
||||
expect(pac).toContain("--source-commit \"$SOURCE_COMMIT\"");
|
||||
expect(pac).toContain("--confirm");
|
||||
expect(pac).not.toMatch(/PostSync|hostPath|docker\.sock|\/var\/run\/docker|127\.0\.0\.1:2019|caddy reload|docker exec/u);
|
||||
});
|
||||
|
||||
test("Secret distribution owns the sourceRef and targetKey", () => {
|
||||
const secrets = Bun.YAML.parse(readFileSync(`${rootPath()}/config/secrets-distribution.yaml`, "utf8"));
|
||||
const binding = secrets.kubernetesSecrets.find((item: Record<string, unknown>) => item.name === "public-edge-trans");
|
||||
expect(binding).toMatchObject({
|
||||
targetId: "public-edge-delivery-nc01",
|
||||
secretName: "public-edge-trans",
|
||||
data: [{
|
||||
sourceRef: "unidesk-host-k8s.env",
|
||||
sourceKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
targetKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test("confirm creates a commit archive and uses only controlled trans operations", () => {
|
||||
const fixture = mkdtempSync(join(tmpdir(), "public-edge-delivery-fixture-"));
|
||||
temporaryRoots.push(fixture);
|
||||
write(fixture, "config/platform-infra/public-edge.yaml", Bun.YAML.stringify({
|
||||
defaults: { targetId: "NC01" },
|
||||
targets: { NC01: { route: "NC01" } },
|
||||
delivery: {
|
||||
enabled: true,
|
||||
pipelineId: "platform-infra-gitea-nc01",
|
||||
sourceCommitEnvironment: "SOURCE_COMMIT",
|
||||
transport: {
|
||||
hostCwd: "/root",
|
||||
secretRef: {
|
||||
configRef: "config/secrets-distribution.yaml",
|
||||
targetId: "public-edge-delivery-nc01",
|
||||
secretName: "public-edge-trans",
|
||||
targetKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
},
|
||||
},
|
||||
hostHelper: {
|
||||
archiveRoot: "/root/unidesk/.state/public-edge-delivery",
|
||||
entrypoint: "scripts/native/cicd/apply-platform-infra-public-edge-archive.mjs",
|
||||
sourcePaths: ["config", "scripts", "package.json", "bun.lock"],
|
||||
},
|
||||
},
|
||||
}));
|
||||
write(fixture, "config/secrets-distribution.yaml", Bun.YAML.stringify({
|
||||
targets: [{ id: "public-edge-delivery-nc01", enabled: true }],
|
||||
kubernetesSecrets: [{
|
||||
name: "public-edge-trans",
|
||||
targetId: "public-edge-delivery-nc01",
|
||||
secretName: "public-edge-trans",
|
||||
data: [{
|
||||
sourceRef: "unidesk-host-k8s.env",
|
||||
sourceKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
targetKey: "UNIDESK_SSH_CLIENT_TOKEN",
|
||||
}],
|
||||
}],
|
||||
}));
|
||||
write(fixture, "scripts/native/cicd/apply-platform-infra-public-edge-archive.mjs", "process.exit(0);\n");
|
||||
write(fixture, "scripts/trans", `#!/bin/sh
|
||||
set -eu
|
||||
if [ "$2" = "sh" ] && [ "\${3-}" != "--" ]; then
|
||||
cat >/dev/null
|
||||
printf '%s\\n' '{"ok":true,"mutation":true}'
|
||||
else
|
||||
printf '%s\\n' '{"ok":true}'
|
||||
fi
|
||||
`);
|
||||
chmodSync(join(fixture, "scripts/trans"), 0o755);
|
||||
write(fixture, "package.json", "{}\n");
|
||||
write(fixture, "bun.lock", "");
|
||||
git(["init", "-b", "master"], fixture);
|
||||
git(["config", "user.name", "UniDesk Test"], fixture);
|
||||
git(["config", "user.email", "unidesk-test@example.invalid"], fixture);
|
||||
git(["add", "."], fixture);
|
||||
git(["commit", "-m", "fixture"], fixture);
|
||||
const sourceCommit = gitHead(fixture);
|
||||
const result = spawnSync("bun", [
|
||||
"scripts/native/cicd/deliver-platform-infra-public-edge.mjs",
|
||||
"--source-root", fixture,
|
||||
"--source-commit", sourceCommit,
|
||||
"--confirm",
|
||||
], {
|
||||
cwd: rootPath(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
SOURCE_COMMIT: sourceCommit,
|
||||
UNIDESK_SSH_CLIENT_TOKEN: "fixture-token",
|
||||
},
|
||||
timeout: 30_000,
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
const output = JSON.parse(result.stdout);
|
||||
expect(output).toMatchObject({
|
||||
ok: true,
|
||||
action: "platform-infra-public-edge-delivery",
|
||||
mutation: true,
|
||||
sourceCommit,
|
||||
runtime: { ok: true, mutation: true },
|
||||
});
|
||||
expect(output.archive.sha256).toMatch(/^sha256:[0-9a-f]{64}$/u);
|
||||
});
|
||||
});
|
||||
|
||||
function gitHead(cwd: string): string {
|
||||
const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
|
||||
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function write(root: string, relativePath: string, content: string) {
|
||||
const path = join(root, relativePath);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content);
|
||||
}
|
||||
|
||||
function git(args: string[], cwd: string) {
|
||||
const result = spawnSync("git", args, { cwd, encoding: "utf8" });
|
||||
if (result.status !== 0) throw new Error(result.stderr || result.stdout);
|
||||
}
|
||||
Reference in New Issue
Block a user